├── .gitignore ├── preload.py ├── templates ├── macros.jinja ├── sampleImage.jinja ├── modelInfo.jinja ├── permissions.jinja ├── cardlist.jinja ├── modelTitle.jinja ├── downloadQueue.jinja ├── modelbasicinfo.jinja └── infotext.jinja ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── auto_close.yml ├── install.py ├── scripts ├── civsfz_shared.py ├── civsfz_color.py ├── civsfz_settings.py ├── civsfz_downloader.py ├── civsfz_filemanage.py └── civsfz_api.py ├── style.css ├── javascript └── civsfz-html.js ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .vscode/ 3 | test.py 4 | search_history.json 5 | api_cache.sqlite 6 | conditions_history.json 7 | keyword_history.json 8 | standalone.py 9 | favoriteUsers.txt 10 | bannedUsers.txt -------------------------------------------------------------------------------- /preload.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | def preload(parser: argparse.ArgumentParser): 4 | pass 5 | # parser.add_argument("--civsfz-api-key", type=str, 6 | # help="API key of Civita", default=None) -------------------------------------------------------------------------------- /templates/macros.jinja: -------------------------------------------------------------------------------- 1 | {% macro bgColor(basemodel, dict_modelprop) %} 2 | {# Return: Color for base model #} 3 | {# Use trim filter for returned value #} 4 | {% for key, val in dict_modelprop.items() %} 5 | {% if loop.last or key in basemodel %} 6 | var({{ val }}) 7 | {% break %} {# Using break requires Loop Control extension #} 8 | {% endif %} 9 | {% endfor %} 10 | {% endmacro %} 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | import launch 2 | import platform 3 | 4 | if not launch.is_installed("colorama"): 5 | launch.run_pip("install colorama", "colorama: requirements for CivBrowser") 6 | if not launch.is_installed("send2trash"): 7 | launch.run_pip("install Send2Trash", "Send2Trash: requirements for CivBrowser") 8 | if not launch.is_installed("jinja2"): 9 | launch.run_pip("install jinja2", "jinja2: requirements for CivBrowser") 10 | #if not launch.is_installed("requests-cache"): 11 | # launch.run_pip("install requests-cache", "requests-cache: requirements for CivBrowser") 12 | system = platform.system() 13 | if system == 'Windows': 14 | pass 15 | elif system == 'Linux': 16 | pass 17 | elif system == 'Darwin': 18 | pass 19 | else: 20 | pass -------------------------------------------------------------------------------- /.github/workflows/auto_close.yml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | 6 | jobs: 7 | close-issues: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/stale@v5 14 | with: 15 | days-before-issue-stale: 14 16 | days-before-issue-close: 7 17 | stale-issue-label: "stale" 18 | stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." 19 | close-issue-message: "This issue was closed because it has been inactive for 14 days." 20 | days-before-pr-stale: -1 21 | days-before-pr-close: -1 22 | repo-token: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment (please complete the following information):** 27 | - OS: [e.g. Windows 11] 28 | - Browser [e.g. chrome, safari] 29 | - UI and Version: [e.g. SD web UI 1.7.0 ] 30 | - CivBrowser Version: [e.g. 1.10.0] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /templates/sampleImage.jinja: -------------------------------------------------------------------------------- 1 | {# Sample image and infotext #} 2 | {# pic nsfw #} 3 | {%- set style='style="vertical-align: top;width: 35%;height: 30em;object-fit: contain;"' %} 4 | {%- if pic['meta'] %} 5 | {%- set style='style="cursor: copy;vertical-align: top;width: 35%;height: 30em;object-fit: contain;" onclick="civsfz_send2txt2img(this.dataset.infotext);"' %} 6 | {%- endif %} 7 | {%- if nsfw %} 8 |
9 | {%- else %} 10 |
11 | {%- endif %} 12 | {%- if pic['type'] == 'image' %} 13 | 14 | {%- else %} 15 | 20 | {%- endif %} 21 | {%- if pic['meta'] %} 22 |
23 | {{ metaHtml }} 24 |
25 | {%- endif %} 26 |
-------------------------------------------------------------------------------- /templates/modelInfo.jinja: -------------------------------------------------------------------------------- 1 | {# Model Information #} 2 | {{ js }} 3 |
4 | Model Info Version: {{ modelInfo["infoVersion"] }} 5 |
6 |
7 |

8 | {%- if modelInfo['nsfw'] %} 9 | NSFW
10 | {%- endif %} 11 | {{ modelInfo["name"]|e }} 12 |

13 |
14 | Model Details 15 |
16 |

Permissions

17 | {{ permissions }} 18 |

{{ modelInfo["allow"]|e }}

19 |
20 |
21 | {{- basicInfo }} 22 | Download here 23 |
24 |
25 |

Model description

26 |

{{ modelInfo["description"] }}

27 |
28 | {%- if modelInfo["versionDescription"] %} 29 |
30 |

Version description

31 |

{{ modelInfo["versionDescription"] }}

32 |
33 | {%- endif %} 34 |
35 |
36 |

Images

37 |
38 | Sample Images 39 |

Clicking on the image sends infotext to txt2img. If local, copy to clipboard

40 |
41 | {{- samples }} 42 |
43 |
44 |
45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /templates/permissions.jinja: -------------------------------------------------------------------------------- 1 | {# Permissions #} 2 | {%- set chrCheck = '✅' %} {# ✅ #} 3 | {%- set chrCross = '❌' %} {# ❌ #} 4 |
5 |

Check the source license yourself.

6 |

This model permits users to:
7 | {%- if allowNoCredit %} {{ chrCheck }} {% else %} {{ chrCross }} {% endif %} : Use the model without crediting the creator
8 | {%- if canSellImages %} {{ chrCheck }} {% else %} {{ chrCross }} {% endif %} : Sell images they generate
9 | {%- if canRent %} {{ chrCheck }} {% else %} {{ chrCross }} {% endif %} : Run on services that generate images for money
10 | {%- if canRentCivit %} {{ chrCheck }} {% else %} {{ chrCross }} {% endif %} : Run on Civitai
11 | {%- if allowDerivatives %} {{ chrCheck }} {% else %} {{ chrCross }} {% endif %} : Share merges using this model
12 | {%- if canSell %} {{ chrCheck }} {% else %} {{ chrCross }} {% endif %} : Sell this model or merges using this model
13 | {%- if allowDifferentLicense %} {{ chrCheck }} {% else %} {{ chrCross }} {% endif %} : Have different permissions when sharing merges model
14 |

15 |
16 |
17 |

18 | {%- if not allowNoCredit %} Creator credit required
{% endif %} 19 | {%- if not canSellImages %} No selling images
{% endif %} 20 | {%- if not canRentCivit %} No Civitai generation
{% endif %} 21 | {%- if not canRent %} No generation services
{% endif %} 22 | {%- if not canSell %} No selling models
{% endif %} 23 | {%- if not allowDerivatives %} No sharing merges
{% endif %} 24 | {%- if not allowDifferentLicense %} Same permissions required
{% endif %} 25 |

26 |
27 | -------------------------------------------------------------------------------- /templates/cardlist.jinja: -------------------------------------------------------------------------------- 1 | {{ forTrigger }} 2 |
3 | {%- for card in cards if card.matchLevel %} 4 | {%- set nsfw = 'civsfz-cardnsfw' if card['isNsfw'] else '' %} 5 | {%- if card['have'] == 'new' %} 6 | {%- set alreadyhave = 'civsfz-modelcardalreadyhave' %} 7 | {%- elif card['have'] == 'old' %} 8 | {%- set alreadyhave = 'civsfz-modelcardalreadyhad' %} 9 | {%- endif %} 10 |
11 | {%- if card['imgType'] == 'image' %} 12 | 13 | {%- elif card['imgType'] == 'video' %} 14 | 19 | {%- else %} 20 | 21 | {%- endif %} 22 |
23 | {{ card['name'] }} 24 | {{ card['creator'] }} 25 |
26 |
{{ card['type'] }}
27 |
{{ card['baseModel'] }}
28 | {%- if card['ea'] == 'in' %} 29 |
EA
30 | {%- elif card['ea'] == 'out' %} 31 |
EA
32 | {%- endif %} 33 | {%- if card['favorite'] %} 34 |
⭐️
35 | {%- endif %} 36 | {%- if card['ngUser'] %} 37 |
🚷
38 | {%- endif %} 39 |
40 | {%- endfor %} 41 |
42 | -------------------------------------------------------------------------------- /templates/modelTitle.jinja: -------------------------------------------------------------------------------- 1 | {%- from 'macros.jinja' import bgColor %} 2 | {%- set modelcolor = "var(--civsfz-color-for-" + baseModel|replace(".", "_")|replace(" ", "_") + ")" %} 3 |
4 |
5 | {{ baseModel|e }} 6 |
7 | 8 | {%- if not ea == "" %} 9 |
11 |   Early Access   12 |
13 | {%- else %} 14 |
15 | {%- endif %} 16 |
17 |
{{ modelName|e }}
18 |
19 | {%- if not uploadUser == "" %} 20 |
22 | by {{ uploadUser|e 24 | }} 25 |
26 | {%- endif %} 27 |
29 | {{ versionName|e }} 30 |
31 |
-------------------------------------------------------------------------------- /templates/downloadQueue.jinja: -------------------------------------------------------------------------------- 1 | {% macro OPEN_FOLDER(file) -%} 2 | 📂 {# 📂 #} 4 | {%- endmacro %} 5 | {% macro CANCEL(file) -%} 6 | {# ❌ #} 8 | {%- endmacro %} 9 | {% set tdStyle1 = 'style="width: 1%;padding:0.4em;white-space: nowrap;"' %} 10 | {% set tdStyle5 = 'style="padding:0.4em; 11 | max-width: 0; overflow: hidden;text-overflow: ellipsis;white-space: nowrap;"' %} 12 | 13 |
14 | 15 | {# #} 16 | {%- for item in resultQ %} 17 | {% set tdStyle3 = 'style="width: 1%;white-space: nowrap;padding:0.4em;background-color: ForestGreen"' 18 | if item['result'] == "Succeeded" 19 | else 'style="width: 1%;white-space: nowrap;padding:0.4em;background-color: GoldenRod"' %} 20 | {% set tdStyle4 = 'style="width: 1%;padding:0.4em;white-space: nowrap;background-image: linear-gradient(90deg, Gray 0%, DimGray {:.0%}, 21 | SlateGray {:.0%}, LightSlateGray 100%);"'.format(item['expiration'],item['expiration']) %} 22 | 23 | 24 | 25 | 26 | 27 | 28 | {%- endfor %} 29 | {%- for item in threadQ %} 30 | {% set tdStyle2 = 'style="width: 1%;padding:0.4em;white-space: nowrap;background-image: linear-gradient(90deg, DarkGreen 0%, ForestGreen {:.0%}, MidnightBlue {:.0%}, MediumBlue 100%);"'.format(item['progress'],item['progress']) %} 31 | 32 | 33 | 34 | 35 | 36 | 37 | {%- endfor %} 38 | {%- for item in waitQ %} 39 | 40 | 41 | 42 | 43 | 44 | 45 | {%- endfor %} 46 |
Download list
{{ item['result'] }}{{ item['filename'] }}{{ OPEN_FOLDER(item['folder']) }}{{ item['folder']|e }}
Downloading{{ item['filename'] }}{{ CANCEL(item['path']) }}{{ OPEN_FOLDER(item['folder']) }}{{ item['folder']|e }}
Waiting{{ item['filename'] }}{{ CANCEL(item['path']) }}{{ OPEN_FOLDER(item['folder']) }}{{ item['folder']|e }}
47 |
48 | -------------------------------------------------------------------------------- /templates/modelbasicinfo.jinja: -------------------------------------------------------------------------------- 1 | {% set copy = '📋' %} {# 📋 #} 2 | {% set tdStyle1 = 'style="max-width:20em;min-width:7em;padding:0.2em;"' %} 3 | 4 |
5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | {%- if 'creator' in modelInfo -%} 17 | 22 | {%- else -%} 23 | 24 | {%- endif -%} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
Model attributes
ID{{ copy }} 10 | {{ modelInfo["id"] }} 11 | 12 |
Uploaded by{{ copy }} 18 | 19 | {{ modelInfo['creator']['username']|e }} 20 | 21 | No creator information
Published{{ published }}
NSFW level{{ strNsfw }}
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {# 53 | 54 | 55 | #} 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 84 | 85 | 86 | 87 |
Version attributes
ID{{ copy }}{{ modelInfo["versionId"] }}
Name{{ modelInfo["versionName"]|e }}
Size{{ (modelInfo["modelVersions"][0]["files"][fileIndex]["sizeKB"] * 1024) |filesizeformat|e }}
Hash{{ copy -}}{{ modelInfo["modelVersions"][0]["files"][fileIndex]["hashes"]["AutoV3"]|lower|e }}
NSFW level{{ strVNsfw }}
Type{{ modelInfo["type"]|e }}
Base Model{{ modelInfo["baseModel"]|e }}
Tags 72 | {%- for tag in modelInfo['tags'] %} 73 | 74 | {{ copy -}} 75 | {{ tag|e -}} 76 | 77 | {%- if not loop.last %} 78 | , 79 | {%- else %} 80 | 81 | {%- endif %} 82 | {%- endfor %} 83 |
Trained Tags 88 | {%- for trainedW in modelInfo['trainedWords'] %} 89 |

{{ copy }}{{ trainedW |e }}

90 | {%- endfor %} 91 |
92 | 93 | 94 | 95 |
-------------------------------------------------------------------------------- /templates/infotext.jinja: -------------------------------------------------------------------------------- 1 | {# meta to infotext #} 2 | {% set copy = '📋' %} {# 📋 #} 4 | 5 | {%- macro extract_var(var) %} 6 | {%- if var is mapping%} 7 | {{ "{" }} 8 | {%- for key, value in var.items() %} 9 | 10 | {{ key|e }}: {{ copy }}{{ value|e }}{{ ", " if not loop.last else "" }} 11 | 12 | {%- endfor -%} 13 | {{- "}" -}} 14 | {% elif var is iterable and var is not string %} 15 | {%- for item in infotext['models'] %} 16 | 17 | {{ item|e }}{{ ", " if not loop.last else "" }} 18 | 19 | {% endfor -%} 20 | {%- else %} 21 | 22 | {{ var|e }} 23 | 24 | {%- endif %} 25 | {%- endmacro%} 26 | 27 | {%- if infotext['prompt'] is not none %} 28 |

29 | prompt: {{ infotext['prompt']|e }} 30 |

31 | {%- endif %} 32 | 33 | {%- if infotext['negativePrompt'] is not none %} 34 |

35 | negativePrompt: {{ infotext['negativePrompt']|e }} 36 |

37 | {%- endif %} 38 | 39 |

40 | {%- for key, value in infotext.items() %} 41 | {%- if not key in ('prompt','negativePrompt', 'resources', 'hashes', 'comfy', 'models', 'additionalResources', 'civitaiResources', 'TI hashes') %} 42 | {%- if value is not none %} 43 | 44 | {{ key|e }}: {{ value|e }}{{ ", " if not loop.last else "" }} 45 | 46 | {%- endif %} 47 | {%- endif %} 48 | {%- endfor %} 49 |

50 | 51 | {%- if 'models' in infotext %} 52 |

53 | models: 54 | {{- extract_var(infotext['models']) }} 55 |

56 | {%- endif %} 57 | {%- if 'hashes' in infotext %} 58 |

59 | hashes: 60 | {{- extract_var(infotext['hashes']) }} 61 |

62 | {%- endif %} 63 | {%- if 'TI hashes' in infotext %} 64 |

65 | {{- extract_var(infotext['TI hashes']) }} 66 |

67 | {%- endif %} 68 | {%- if 'resources' in infotext %} 69 |

70 | resources: {{ "[" }} 71 | {%- for item in infotext['resources'] %} 72 | {{- extract_var(item) -}} 73 | {{- ", " if not loop.last else "" }} 74 | {%- endfor -%} 75 | {{ "]" }} 76 |

77 | {%- endif %} 78 | {%- if 'additionalResources' in infotext %} 79 |

80 | additionalResources: {{ "[" }} 81 | {%- for item in infotext['additionalResources'] %} 82 | {{- extract_var(item) }}{{ ", " if not loop.last else "" }} 83 | {%- endfor -%} 84 | {{ "]" }} 85 |

86 | {%- endif %} 87 | {%- if 'civitaiResources' in infotext %} 88 |

89 | civitaiResources: {{ "[" }} 90 | {%- for item in infotext['civitaiResources'] %} 91 | {{- extract_var(item) }}{{ ", " if not loop.last else "" }} 92 | {%- endfor -%} 93 | {{ "]" }} 94 |

95 | {%- endif %} 96 | {%- if 'comfy' in infotext %} 97 |

98 | 99 | comfy: {{ infotext['comfy']|e }} 100 | 101 |

102 | {%- endif %} 103 | -------------------------------------------------------------------------------- /scripts/civsfz_shared.py: -------------------------------------------------------------------------------- 1 | VERSION = "v2.9.2" 2 | 3 | platform = "A1111" 4 | forge_version = None 5 | card_no_preview = "./file=html/card-no-preview.png" # Neo uses jpg 6 | 7 | from html.parser import HTMLParser 8 | 9 | import gradio as gr 10 | # GRADIO_VERSION = gr.__version__ 11 | GR_V440 = True if "4.40" in gr.__version__ else False 12 | 13 | try: 14 | from modules_forge.forge_version import version as forge_version 15 | except ImportError: 16 | from modules.cmd_args import parser 17 | if parser.description: 18 | platform = "SD.Next" 19 | pass 20 | else: 21 | if forge_version in ["classic", "neo"]: 22 | platform = "Forge Classic" 23 | card_no_preview = "./file=html/card-no-preview.jpg" 24 | else: 25 | platform = "Forge" 26 | # print(f"Working on {platform=} {forge_version=}") 27 | 28 | from modules.shared import opts as opts 29 | try: 30 | # SD web UI >= v1.6.0-RC 31 | # Forge 32 | # Forge Classic/neo 33 | from modules.shared_cmd_options import cmd_opts as cmd_opts 34 | if forge_version in ["classic", "neo"]: 35 | from modules.sd_models import model_path 36 | if getattr(cmd_opts, "ckpt_dir", None) is None: 37 | setattr(cmd_opts, "ckpt_dir", model_path) 38 | import os 39 | from modules import paths 40 | if getattr(cmd_opts, "hypernetwork_dir", None) is None: 41 | setattr( 42 | cmd_opts, 43 | "hypernetwork_dir", 44 | os.path.abspath(os.path.join(paths.models_path, "hypernetworks")), 45 | ) 46 | if getattr(cmd_opts, "vae_dir", None) is None: 47 | setattr( 48 | cmd_opts, 49 | "vae_dir", 50 | os.path.abspath(os.path.join(paths.models_path, "VAE")), 51 | ) 52 | #print(f"{cmd_opts=}") 53 | except ImportError: 54 | # SD web UI < v1.6.0-RC 55 | # SD.Next 56 | from modules.shared import cmd_opts as cmd_opts 57 | 58 | try: 59 | from modules.hashes import calculate_sha256_real as calculate_sha256 60 | except ImportError: 61 | from modules.hashes import calculate_sha256 as calculate_sha256 62 | 63 | 64 | def read_timeout(): 65 | return 15, getattr(opts, "civsfz_request_timeout", 30) 66 | 67 | class HTML2txt(HTMLParser): 68 | text = "" 69 | prevEndTag = "" 70 | 71 | def __init__(self, start_text: str = ""): 72 | super().__init__() 73 | self.text = start_text 74 | self.prevEndTag = "" 75 | 76 | def handle_starttag(self, tag, attrs): 77 | if tag in ["li"]: 78 | self.text += " - " # indent 79 | elif tag in ["hr"]: 80 | self.text += "----------\n" 81 | self.prevEndTag = "" 82 | 83 | def handle_endtag(self, tag): 84 | if tag in [ 85 | "p", 86 | "br", 87 | "title", 88 | "h1", 89 | "h2", 90 | "h3", 91 | "h4", 92 | "h5", 93 | "h6", 94 | "ul", 95 | "ol", 96 | "dl", 97 | "dir", 98 | "menu", 99 | "table", 100 | "li", 101 | "caption", 102 | "thread", 103 | "tr", 104 | "pre", 105 | ]: 106 | if self.prevEndTag in ["p"] and tag in ["li"]: 107 | pass 108 | else: 109 | self.text += "\n" 110 | else: 111 | self.text += " " 112 | self.prevEndTag = tag 113 | 114 | def handle_data(self, data): 115 | self.text += data 116 | 117 | def addText(self, addedText=""): 118 | self.text += addedText 119 | 120 | def setInnerText(self, text: str = ""): 121 | # for reset 122 | self.text = text 123 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | 2 | .civsfz-custom-property { 3 | --civsfz-shadow-color-default: hsl(225, 15%, 30% , 0.7); 4 | --civsfz-shadow-color-alreadyhave: hsl(180, 80%, 50%, 0.8); 5 | --civsfz-shadow-color-alreadyhad: hsl(180, 60%, 30%, 0.8); 6 | --civsfz-background-color-figcaption: hsl(225, 15%, 30% , 0.8); 7 | --civsfz-background-color-early-access-in: hsl(225, 80%, 40%, 0.8); 8 | --civsfz-hover-scale: 1.5; 9 | --civsfz-card-width: 8; 10 | --civsfz-card-height: 12; 11 | --civsfz-card-size-unit: 1em; 12 | } 13 | 14 | .civsfz-modellist { 15 | display: flex; 16 | flex-wrap: wrap; 17 | } 18 | 19 | .civsfz-modellist figure { 20 | margin: 4px; 21 | transition: transform .3s ease-out; 22 | cursor: pointer; 23 | } 24 | 25 | .civsfz-modelcard { 26 | position: relative; 27 | overflow: hidden; 28 | } 29 | 30 | .civsfz-modelcard:hover { 31 | transform: scale(var(--civsfz-hover-scale), var(--civsfz-hover-scale)); 32 | position: relative; 33 | overflow: visible; 34 | /* need for z-index */ 35 | z-index: var(--layer-5); 36 | box-shadow: 0 0 4px 6px var(--civsfz-shadow-color-default); 37 | } 38 | 39 | .civsfz-modelcardalreadyhave { 40 | box-shadow: 0 0 1px 4px var(--civsfz-shadow-color-alreadyhave); 41 | } 42 | .civsfz-modelcardalreadyhave:hover { 43 | box-shadow: 0 0 4px 6px var(--civsfz-shadow-color-alreadyhave); 44 | } 45 | .civsfz-modelcardalreadyhad { 46 | box-shadow: 0 0 1px 4px var(--civsfz-shadow-color-alreadyhad); 47 | } 48 | .civsfz-modelcardalreadyhad:hover { 49 | box-shadow: 0 0 4px 6px var(--civsfz-shadow-color-alreadyhad); 50 | } 51 | 52 | /* 53 | .civsfz-bgcolor-base { 54 | background-color: var(--civsfz-figcaption-background-color); 55 | } 56 | .civsfz-bgcolor-SD1 { 57 | background-color: var(--civsfz-sd1-background-color); 58 | } 59 | .civsfz-bgcolor-SD2 { 60 | background-color: var(--civsfz-sd2-background-color); 61 | } 62 | .civsfz-bgcolor-SD3 { 63 | background-color: var(--civsfz-sd3-background-color); 64 | } 65 | .civsfz-bgcolor-SD35 { 66 | background-color: var(--civsfz-sd35-background-color); 67 | } 68 | .civsfz-bgcolor-SDXL { 69 | background-color: var(--civsfz-sdxl-background-color); 70 | } 71 | .civsfz-bgcolor-Pony { 72 | background-color: var(--civsfz-pony-background-color); 73 | } 74 | .civsfz-bgcolor-Illustrious { 75 | background-color: var(--civsfz-illustrious-background-color); 76 | } 77 | .civsfz-bgcolor-Flux1 { 78 | background-color: var(--civsfz-flux1-background-color); 79 | } 80 | */ 81 | .civsfz-modelcard img { 82 | /*filter: blur(6px);*/ 83 | width: calc(var(--civsfz-card-width) * var(--civsfz-card-size-unit)); 84 | height: calc(var(--civsfz-card-height) * var(--civsfz-card-size-unit)); 85 | object-fit: cover; 86 | } 87 | 88 | .civsfz-modelcard video { 89 | /*filter: blur(6px);*/ 90 | width: calc(var(--civsfz-card-width) * var(--civsfz-card-size-unit)); 91 | height: calc(var(--civsfz-card-height) * var(--civsfz-card-size-unit)); 92 | object-fit: cover; 93 | } 94 | 95 | .civsfz-modelcard figcaption { 96 | position: absolute; 97 | bottom: 5px; 98 | left: 5px; 99 | right: 5px; 100 | word-break: break-word; 101 | background-color: var(--civsfz-background-color-figcaption); 102 | /*transition: bottom .3s ease-out;*/ 103 | max-height: 60%; 104 | } 105 | 106 | .civsfz-modelcard:hover figcaption { 107 | bottom: initial; 108 | max-height: initial; 109 | } 110 | 111 | .civsfz-modelcard .civsfz-basemodel { 112 | position: absolute; 113 | top: 0px; 114 | right: 0px; 115 | word-break: break-word; 116 | font-size: smaller; 117 | max-width: 70%; 118 | text-align: right; 119 | } 120 | .civsfz-modelcard:hover .civsfz-basemodel { 121 | top: -1.5em; 122 | } 123 | .civsfz-modelcard .civsfz-modeltype { 124 | position: absolute; 125 | top: 0px; 126 | left: 0px; 127 | word-break: break-word; 128 | font-size: smaller; 129 | background-color: var(--civsfz-background-color-figcaption); 130 | } 131 | .civsfz-modelcard:hover .civsfz-modeltype { 132 | top: -1.5em; 133 | } 134 | 135 | 136 | .civsfz-modelcard .civsfz-creator-flag { 137 | position: absolute; 138 | bottom: 4px; 139 | right: -3px; 140 | word-break: break-word; 141 | /* color: gold; */ 142 | font-size: x-large; 143 | text-shadow: -3px 3px 2px black; 144 | } 145 | .civsfz-modelcard:hover .civsfz-creator-flag { 146 | bottom: -1em; 147 | right: -0.5em; 148 | opacity: 90%; 149 | transform:rotate(20deg); 150 | } 151 | 152 | .civsfz-modelcard .civsfz-early-access-out { 153 | position: absolute; 154 | top: 1.4em; 155 | right: 0em; 156 | font-size: smaller; 157 | background-color: var(--civsfz-early-access-out-background-color); 158 | } 159 | .civsfz-modelcard:hover .civsfz-early-access-out { 160 | top: -1.5em; 161 | right: -1.2em; 162 | } 163 | .civsfz-modelcard .civsfz-early-access-in { 164 | position: absolute; 165 | top: 1.4em; 166 | right: 0em; 167 | word-break: break-word; 168 | font-size: smaller; 169 | background-color: var(--civsfz-background-color-early-access-in); 170 | } 171 | .civsfz-modelcard:hover .civsfz-early-access-in { 172 | top: -1.5em; 173 | right: -1.2em; 174 | } 175 | 176 | .civsfz-nsfw img { 177 | filter: blur(12px); 178 | } 179 | 180 | .civsfz-cardnsfw img { 181 | filter: blur(6px); 182 | } 183 | 184 | .civsfz-nsfw video { 185 | filter: blur(12px); 186 | } 187 | 188 | .civsfz-cardnsfw video { 189 | filter: blur(6px); 190 | } 191 | 192 | #civsfz_tab-element .civsfz-tabbar:first-child { 193 | position: -webkit-sticky; /* Safari */ 194 | position: sticky; 195 | top: 0em; 196 | z-index: 2; 197 | } 198 | [id^="civsfz_model-navigation"] .civsfz-navigation-buttons { 199 | position: -webkit-sticky; 200 | /* Safari */ 201 | position: sticky; 202 | top: 3em; 203 | z-index: 1; 204 | } 205 | [id^="civsfz_model-navigation"] .civsfz-jump-page-control { 206 | position: -webkit-sticky; 207 | /* Safari */ 208 | position: sticky; 209 | bottom: 0px; 210 | z-index: 1; 211 | } 212 | [id^="civsfz_model-data"] .civsfz-save-buttons { 213 | position: -webkit-sticky; /* Safari */ 214 | position: sticky; 215 | top: 3em; 216 | z-index: 1; 217 | } 218 | [id^="civsfz_model-data"] .civsfz-back-to-top { 219 | text-align: right; 220 | position: -webkit-sticky; /* Safari */ 221 | position:sticky; 222 | top: 7em; 223 | z-index: 2; 224 | float: inline-end; 225 | pointer-events: none; 226 | height: 0em; 227 | } 228 | .civsfz-sticky-parent { 229 | display: initial; 230 | } 231 | .civsfz-sticky-element { 232 | backdrop-filter: brightness(0.35); 233 | padding: 0.5em 0; 234 | } 235 | /*https: //github.com/gradio-app/gradio/issues/3337#issuecomment-1449589285*/ 236 | .civsfz-small-buttons { 237 | margin: 1.2em 0em 0.55em 0em !important; 238 | max-width: 2.5em; 239 | min-width: 2.5em !important; 240 | height: 2.4em; 241 | } 242 | .civsfz-small-buttons2 { 243 | margin: 0em 0em 0.0em 0em !important; 244 | max-width: 2.5em; 245 | min-width: 2.5em !important; 246 | height: 2.4em; 247 | } 248 | 249 | .civsfz-msg { 250 | /*align-self: center;*/ 251 | text-align: center; 252 | background-color: maroon; 253 | margin: 1.1em 0em 0em 0em !important; 254 | max-width: 8em; 255 | min-width: 8em !important; 256 | } 257 | 258 | .civsfz-tooltip { 259 | position: relative; 260 | display: inline-block; 261 | /* border-bottom: 1px dotted black; */ 262 | } 263 | .civsfz-tooltip .civsfz-tooltiptext { 264 | visibility: hidden; 265 | width: max-content; 266 | max-width: 100%; 267 | bottom: 100%; 268 | right: 0%; 269 | background-color: var(--civsfz-background-color-figcaption); 270 | text-align: left; 271 | padding: 2px 5px; 272 | border-radius: 6px; 273 | position: absolute; 274 | z-index: 1; 275 | font-size: x-small; 276 | } 277 | .civsfz-tooltip:hover .civsfz-tooltiptext { 278 | visibility: visible; 279 | } 280 | -------------------------------------------------------------------------------- /scripts/civsfz_color.py: -------------------------------------------------------------------------------- 1 | import colorsys 2 | from scripts.civsfz_shared import opts 3 | 4 | 5 | def hex_color_hsl_to_rgb(h, s, l): 6 | # param order h,s,l not h,l,s 7 | if h > 1.0: 8 | h = h % 360 / 360 9 | if l > 1.0: 10 | l = max(min(l / 100, 1.0), 0) 11 | if s > 1.0: 12 | s = max(min(s / 100, 1.0), 0) 13 | (r, g, b) = colorsys.hls_to_rgb(h, l, s) 14 | return f"#{round(r*255):02x}{round(g*255):02x}{round(b*255):02x}" 15 | 16 | 17 | def hex_color_hsl_to_rgba(h, s, l, opacity=None): 18 | # param order h,s,l not h,l,s 19 | ret = hex_color_hsl_to_rgb(h, s, l) 20 | if opacity is not None: 21 | if opacity > 1.0: 22 | opacity = max(min(opacity / 100, 1.0), 0) 23 | alpha = f"{round(opacity*255):02x}" 24 | else: 25 | alpha = "" 26 | return f"{ret}{alpha}" 27 | 28 | 29 | def hls_from_hex(hexrgb): 30 | (r, g, b) = (int(hexrgb[1:3], 16), int(hexrgb[3:5], 16), int(hexrgb[5:7], 16)) 31 | (r, g, b) = (x / 255 for x in (r, g, b)) 32 | return colorsys.rgb_to_hls(r, g, b) 33 | 34 | 35 | # Initial values 36 | familyColor: dict = { 37 | "family1": { 38 | "value": [ 39 | "SD 1.5", 40 | "SD 1.5 LCM", 41 | "SD 1.5 Hyper", 42 | "SD 1.4", 43 | "SD 2.1", 44 | "SD 2.1 768", 45 | "SD 2.0", 46 | "SD 2.0 768", 47 | "SD 2.1 Unclip", 48 | ], 49 | "color": hex_color_hsl_to_rgb(100, 100, 40), 50 | }, 51 | "family2": { 52 | "value": [ 53 | "Illustrious", 54 | "Pony", 55 | "Pony V7", 56 | "NoobAI", 57 | "SDXL 1.0", 58 | "SDXL 0.9", 59 | "SDXL 1.0 LCM", 60 | "SDXL Distilled", 61 | "SDXL Turbo", 62 | "SDXL Lightning", 63 | "SDXL Hyper", 64 | ], 65 | "color": hex_color_hsl_to_rgb(15, 100, 45), 66 | }, 67 | "family3": { 68 | "value": [ 69 | "Flux.2 D", 70 | "Flux.1 D", 71 | "Flux.1 S", 72 | "Flux.1 Krea", 73 | "Flux.1 Kontext", 74 | "SD 3.5", 75 | "SD 3.5 Large", 76 | "SD 3.5 Medium", 77 | "SD 3.5 Large Turbo", 78 | "SD 3", 79 | ], 80 | "color": hex_color_hsl_to_rgb(130, 90, 30), 81 | }, 82 | "family4": { 83 | "value": [ 84 | "Wan Video", 85 | "Wan Video 1.3B t2v", 86 | "Wan Video 14B t2v", 87 | "Wan Video 14B i2v 480p", 88 | "Wan Video 14B i2v 720p", 89 | "Wan Video 2.2 TI2V-5B", 90 | "Wan Video 2.2 I2V-A14B", 91 | "Wan Video 2.2 T2V-A14B", 92 | "Wan Video 2.5 T2V", 93 | "Wan Video 2.5 I2V", 94 | ], 95 | "color": hex_color_hsl_to_rgb(300, 90, 45), 96 | }, 97 | "family5": { 98 | "value": [ 99 | "ZImageTurbo", 100 | "Qwen", 101 | ], 102 | "color": hex_color_hsl_to_rgb(279, 38, 48), 103 | }, 104 | "family6": { 105 | "value": [ 106 | "Hunyuan Video", 107 | "Hunyuan 1", 108 | "Mochi", 109 | "SVD", 110 | "SVD XT", 111 | "LTXV", 112 | "CogVideoX", 113 | "HiDream", 114 | "OpenAI", 115 | ], 116 | "color": hex_color_hsl_to_rgb(330, 90, 45), 117 | }, 118 | "non_family": { 119 | "value": [], 120 | "color": hex_color_hsl_to_rgb(250, 14, 30), 121 | }, 122 | } 123 | 124 | def autoColorRotate(hexColor: str, num: int, i: int, hue=30, opacity=None): 125 | (h, l, s) = hls_from_hex(hexColor) 126 | h = h + (hue/(num/3)*(i//4))/360 127 | l = l * ((1 - (i % 4) / 5) * 0.6 + 0.4) 128 | s = s * 0.5/num*(num-i)+0.5 129 | return hex_color_hsl_to_rgba(h, s, l, opacity) 130 | 131 | 132 | def dictBasemodelColors(listBaseModel: list) -> dict: 133 | ret = {} 134 | for name in listBaseModel: 135 | for k, v in familyColor.items(): 136 | family = getattr(opts, "civsfz_" + k, []) 137 | num = len(family) 138 | for baseModel in family: 139 | if name == baseModel: 140 | i = family.index(name) 141 | hexColor = getattr(opts, "civsfz_color_" + k) 142 | ret[name] = autoColorRotate(hexColor, num, i) 143 | # print(f"{name}:{k}-{num}:{i}:{hexColor}:{ret[name]}") 144 | if name not in ret: 145 | ret[name] = opts.civsfz_color_non_family 146 | # print(f"{ret}") 147 | return ret 148 | 149 | 150 | class BaseModelColors: 151 | colors = [ 152 | { 153 | "name": "BASE", 154 | "label": "Background color for model names", 155 | "key": "civsfz_background_color_figcaption", 156 | "property": "--civsfz-background-color-figcaption", 157 | "color": hex_color_hsl_to_rgb(225, 15, 30), 158 | }, 159 | { 160 | "name": "SD 1", 161 | "label": "Background color for SD1 models", 162 | "key": "civsfz_background_color_sd1", 163 | "property": "--civsfz-background-color-sd1", 164 | "color": hex_color_hsl_to_rgb(90, 70, 30), 165 | }, 166 | { 167 | "name": "SD 2", 168 | "label": "Background color for SD2 models", 169 | "key": "civsfz_background_color_sd2", 170 | "property": "--civsfz-background-color-sd2", 171 | "color": hex_color_hsl_to_rgb(90, 60, 40), 172 | }, 173 | { 174 | "name": "SD 3", 175 | "label": "Background color for SD3 models", 176 | "key": "civsfz_background_color_sd3", 177 | "property": "--civsfz-background-color-sd3", 178 | "color": hex_color_hsl_to_rgb(135, 70, 30), 179 | }, 180 | { 181 | "name": "SD 3.", 182 | "label": "Background color for SD3.5 models", 183 | "key": "civsfz_background_color_sd35", 184 | "property": "--civsfz-background-color-sd35", 185 | "color": hex_color_hsl_to_rgb(150, 80, 40), 186 | }, 187 | { 188 | "name": "SDXL", 189 | "label": "Background color for SDXL models", 190 | "key": "civsfz_background_color_sdxl", 191 | "property": "--civsfz-background-color-sdxl", 192 | "color": hex_color_hsl_to_rgb(15, 70, 40), 193 | }, 194 | { 195 | "name": "Pony", 196 | "label": "Background color for Pony models", 197 | "key": "civsfz_background_color_pony", 198 | "property": "--civsfz-background-color-pony", 199 | "color": hex_color_hsl_to_rgb(15, 90, 40), 200 | }, 201 | { 202 | "name": "Illustrious", 203 | "label": "Background color for Illustrious models", 204 | "key": "civsfz_background_color_illustrious", 205 | "property": "--civsfz-background-color-illustrious", 206 | "color": hex_color_hsl_to_rgb(15, 95, 50), 207 | }, 208 | { 209 | "name": "Flux.1", 210 | "label": "Background color for Flux.1 models", 211 | "key": "civsfz_background_color_flux1", 212 | "property": "--civsfz-background-color-flux1", 213 | "color": hex_color_hsl_to_rgb(330, 80, 40), 214 | }, 215 | ] 216 | 217 | def __init__(self) -> None: 218 | pass 219 | 220 | # for macros.jinja 221 | def name_property_dict(self): 222 | ret = {} 223 | for d in self.colors: 224 | ret[d["name"]] = d["property"] 225 | # reverse order for sd3.5 226 | return dict(reversed(list(ret.items()))) 227 | 228 | 229 | # print(f"{BaseModelColors().name_property_dict()}") 230 | -------------------------------------------------------------------------------- /javascript/civsfz-html.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | onUiLoaded(civbrowser_start_it_up); 4 | function civbrowser_start_it_up() { 5 | //make tab sticky 6 | let elem = gradioApp().querySelector('#civsfz_tab-element').firstChild; 7 | elem.classList.add("civsfz-sticky-element"); 8 | elem.classList.add("civsfz-tabbar"); 9 | } 10 | 11 | function civsfz_select_model(model_name) { 12 | console.log(model_name); 13 | // model_name-> selector:tab_id: 14 | const regex1 = /(^.+?)(\d):/; 15 | let match = regex1.exec(model_name); 16 | //console.log(match); 17 | if (match[1] == 'Index') { 18 | civsfz_scroll_to('#civsfz_model-data' + match[2]); 19 | let selector = '#civsfz_eventtext' + match[2] + ' textarea'; 20 | let model_dropdown = gradioApp().querySelector(selector); 21 | if (model_dropdown && model_name) { 22 | /*Force card click event*/ 23 | model_dropdown.value = model_name + ':' + civsfz_getRandomIntInclusive(0, 9999); 24 | updateInput(model_dropdown); 25 | } 26 | } 27 | } 28 | 29 | function civsfz_update_textbox(ID, text) { 30 | //console.log(text) 31 | let selector = ID + ' textarea'; 32 | let textbox = gradioApp().querySelector(selector); 33 | if (textbox) { 34 | /*Force event*/ 35 | textbox.value = text; 36 | updateInput(textbox); 37 | } 38 | } 39 | 40 | function civsfz_cancel_download(path) { 41 | // Cancel download 42 | //console.log(text) 43 | path = decodeURI(path); 44 | let id = '#civsfz_eventtext_dl'; 45 | if (path) { 46 | let text = 'CancelDl??' + path + '??' + civsfz_getRandomIntInclusive(0, 9999); 47 | civsfz_update_textbox(id, text); 48 | } 49 | } 50 | 51 | function civsfz_open_folder(path) { 52 | // Cancel download 53 | //console.log(text) 54 | path = decodeURI(path); 55 | let id = '#civsfz_eventtext_dl'; 56 | if (path) { 57 | let text = 'OpenFolder??' + path + '??' + civsfz_getRandomIntInclusive(0, 9999); 58 | civsfz_update_textbox(id, text); 59 | } 60 | } 61 | 62 | /*https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Math/random#%E5%8C%85%E6%8B%AC%E7%9A%84%E3%81%AB_2_%E3%81%A4%E3%81%AE%E5%80%A4%E3%81%AE%E9%96%93%E3%81%AE%E3%83%A9%E3%83%B3%E3%83%80%E3%83%A0%E3%81%AA%E6%95%B4%E6%95%B0%E3%82%92%E5%BE%97%E3%82%8B*/ 63 | function civsfz_getRandomIntInclusive(min, max) { 64 | min = Math.ceil(min); 65 | max = Math.floor(max); 66 | return Math.floor(Math.random() * (max - min + 1) + min); //The maximum is inclusive and the minimum is inclusive 67 | } 68 | 69 | function civsfz_trigger_event(element, event) { 70 | let e = new Event(event); 71 | Object.defineProperty(e, "target", { value: element }); 72 | //element.focus(); 73 | element.dispatchEvent(e); 74 | } 75 | function civsfz_trigger_key_down(element, key) { 76 | let e = new KeyboardEvent("keydown", { key: key }); 77 | //element.focus(); 78 | element.dispatchEvent(e); 79 | } 80 | 81 | function civsfz_send2txt2img(text, send = true) { 82 | //console.log(text) 83 | text = decodeURI(text); 84 | if (send) { 85 | let response = confirm("Send to txt2img?"); 86 | if (response) { 87 | let prompt = gradioApp().querySelector('#txt2img_prompt textarea'); 88 | let paste = gradioApp().querySelector('#paste'); 89 | if (paste == null) { 90 | //SD.Next 91 | paste = gradioApp().querySelector('#txt2img_paste'); 92 | } 93 | prompt.value = text; 94 | civsfz_trigger_event(prompt, 'input'); 95 | civsfz_trigger_event(paste, 'click'); 96 | //trigger_key_down(prompt, 'Escape'); 97 | } 98 | } else { 99 | return navigator.clipboard.writeText(text) 100 | .then( 101 | function () { 102 | alert("Copied " + text); 103 | } 104 | ).catch( 105 | function (error) { 106 | alert((error && error.message) || "Failed to copy infotext"); 107 | } 108 | ) 109 | } 110 | } 111 | 112 | function civsfz_copyInnerText(node, send = true) { 113 | if (node.nextSibling != null) { 114 | //let ret = navigator.clipboard.writeText(node.nextSibling.innerText; 115 | //alert("Copied infotext"); 116 | if (send) { 117 | let response = confirm("Send to txt2img?"); 118 | if (response) { 119 | let prompt = gradioApp().querySelector('#txt2img_prompt textarea'); 120 | let paste = gradioApp().querySelector('#paste'); 121 | if (paste == null) { 122 | //SD.Next 123 | paste = gradioApp().querySelector('#txt2img_paste'); 124 | } 125 | prompt.value = node.nextElementSibling.innerText; 126 | civsfz_trigger_event(prompt, 'input'); 127 | civsfz_trigger_event(paste, 'click'); 128 | //trigger_key_down(prompt, 'Escape'); 129 | } 130 | } else { 131 | return navigator.clipboard.writeText(node.nextElementSibling.innerText.trim()) 132 | .then( 133 | function () { 134 | alert("Copied " + node.nextElementSibling.innerText.trim()); 135 | } 136 | ).catch( 137 | function (error) { 138 | alert((error && error.message) || "Failed to copy infotext"); 139 | } 140 | ) 141 | } 142 | } 143 | } 144 | 145 | function civsfz_overwriteProperties(propertiesText) { 146 | //let propertiesText = gradioApp().querySelector('#' + elem_id + ' textarea').value; 147 | //console.log(elem_id, propertiesText) 148 | let p = propertiesText.split(';'); 149 | let elem = gradioApp().querySelector('.civsfz-custom-property'); 150 | let i = 0 151 | //elements.forEach((elem) => { 152 | elem.style.setProperty('--civsfz-background-color-figcaption', p[i++]); 153 | elem.style.setProperty('--civsfz-shadow-color-default', p[i++] + 'f0'); 154 | elem.style.setProperty('--civsfz-shadow-color-alreadyhave', p[i++] + 'f0'); 155 | elem.style.setProperty('--civsfz-shadow-color-alreadyhad', p[i++] + 'f0'); 156 | elem.style.setProperty('--civsfz-hover-scale', p[i++]); 157 | elem.style.setProperty('--civsfz-card-width', p[i++]); 158 | elem.style.setProperty('--civsfz-card-height', p[i++]); 159 | //Color Family 160 | const fColors = JSON.parse(p[i]); 161 | Object.keys(fColors).forEach(function (key) { 162 | let color = fColors[key] 163 | let prop = '--civsfz-color-for-' + key.replace(/[\ \.]/g, '_') 164 | elem.style.setProperty(prop, color); 165 | }) 166 | //}); 167 | } 168 | 169 | function civsfz_querySelectSetProperty(q, p, c) { 170 | let elements = gradioApp().querySelectorAll(q); 171 | elements.forEach((elem) => { 172 | elem.style.setProperty(p, c); 173 | }); 174 | } 175 | 176 | function civsfz_scroll_to(q) { 177 | const elem = gradioApp().querySelector(q); 178 | if (true) { 179 | const offset = -40; 180 | const y = elem.getBoundingClientRect().top + window.scrollY + offset; 181 | window.scrollTo({ top: y, behavior: 'smooth' }); 182 | } else { 183 | elem.scrollIntoView({ 184 | behavior: 'smooth', 185 | block: 'start', 186 | inline: 'nearest' 187 | }); 188 | } 189 | } 190 | 191 | function civsfz_preview_colors() { 192 | for (var i = 1; i <= 10; i++) { 193 | let elmDropdwn = gradioApp().querySelector('#setting_civsfz_family' + i.toString(10)); 194 | if (elmDropdwn == null) { break; } 195 | let elmColor = gradioApp().querySelector('#setting_civsfz_color_family' + i.toString(10)); 196 | let color = elmColor.querySelector("input").value; 197 | //console.log(color); 198 | let tokens = elmDropdwn.getElementsByClassName("token"); 199 | let len = tokens.length; 200 | let hasBg = len > 0 ? tokens[0].style.getPropertyValue("background") : false; 201 | for (let j=0; j < tokens.length; j++) { 202 | let token = tokens[j]; 203 | if (!hasBg) { 204 | let h_param = 30 / (len / 3) * Math.floor(j/4); 205 | let l_param = (1 - j % 4 / 5)* 0.6 + 0.4; 206 | let s_param = 0.5/len*(len-j) + 0.5; 207 | token.style.setProperty("background",`hsl(from ${color} calc(h + ${h_param}) calc(s*${s_param}) calc(l*${l_param})`); 208 | } else { 209 | token.style.removeProperty("background"); 210 | } 211 | } 212 | } 213 | } 214 | 215 | function civsfz_scroll_and_color(q, id, l ){ 216 | civsfz_scroll_to(q); 217 | civsfz_version_color(id, l); 218 | } 219 | 220 | function civsfz_version_color(id, l) { 221 | //console.log(l); 222 | let versions = JSON.parse(l); 223 | let elmVersionRadio = gradioApp().querySelector(id); 224 | if (elmVersionRadio == null) { return; } 225 | let labels = elmVersionRadio.getElementsByTagName("label"); 226 | for (let j = 0; j < labels.length; j++) { 227 | let label = labels[j]; 228 | // let span = label.getElementsByTagName("span")[0]; 229 | // console.log(label); 230 | let color = "var(--civsfz-color-for-" + versions[j]["base_model"].replace(/[\ \.]/g, '_'); 231 | label.style.setProperty("border-bottom", "solid 4px " + color); 232 | if (versions[j]["have"]) { 233 | label.style.setProperty("border-top", "solid 2px var(--civsfz-shadow-color-alreadyhave)"); 234 | } else { 235 | label.style.removeProperty("border-top"); 236 | } 237 | } 238 | } -------------------------------------------------------------------------------- /scripts/civsfz_settings.py: -------------------------------------------------------------------------------- 1 | import gradio as gr 2 | from modules import script_callbacks, shared, ui_components 3 | from scripts.civsfz_color import BaseModelColors, familyColor 4 | 5 | # SD.Next can not import from civsfz_shared.py 6 | GR_V440 = True if "4.40" in gr.__version__ else False 7 | 8 | def on_ui_settings(): 9 | from scripts.civsfz_shared import platform 10 | from scripts.civsfz_api import APIInformation 11 | 12 | class myOption(shared.OptionInfo): 13 | def __init__(self, text, **kwargs): 14 | super().__init__(text, **kwargs) 15 | self.do_not_save = True 16 | 17 | def js(self, label, js_func): 18 | self.comment_before += ( 19 | f"[{label}]" 20 | ) 21 | return self 22 | 23 | Api = APIInformation() 24 | 25 | civsfz_section = "Civsfz_Browser", "CivBrowser" 26 | # OptionInfo params 27 | # default=None, label="", component=None, component_args=None, onchange=None, section=None, refresh=None, comment_before='', comment_after='', infotext=None, restrict_api=False, category_id=None 28 | # SD.Next dose not have OptionHTML 29 | dict_user_management = ( 30 | { 31 | "civsfz_msg_user_management": shared.OptionHTML( 32 | "

Creator management

" 33 | "Favourite creators and banned creators are stored in text files `favoriteUsers.txt` and `bannedUsers.txt` respectively. " 34 | "To register the creator, go to User Management on the CivBrowser tab.
" 35 | "The previous registration items in here have been discontinued. " 36 | "Previously registered users are re-registered every time the app is started. " 37 | "Therefore, it is not possible to delete previously registered users. " 38 | "To avoid this, edit your `config.json` file and remove the `civsfz_favorite_creators` and `civsfz_ban_creators` lines." 39 | ), 40 | } 41 | if not platform == "SD.Next" 42 | else {} 43 | ) 44 | 45 | dict_api_info = ( 46 | { 47 | "civsfz_msg_html1": shared.OptionHTML( 48 | "

API-Key

" 49 | "The command line option `--civsfz-api-key` is deprecated. " 50 | "We felt that this was a risk because some users might not notice the API-Key being displayed on the console. " 51 | "The API-Key here is saved in the `config.json` file.
" 52 | "If you do not know this, there is a risk of your API-Key being leaked." 53 | ), 54 | } 55 | if not platform == "SD.Next" 56 | else {} 57 | ) 58 | 59 | dict_options1 = { 60 | "civsfz_api_key": shared.OptionInfo( 61 | "", 62 | label="API-Key", 63 | component=gr.Textbox, 64 | component_args={"type": "password"}, 65 | ).info("Note: API-Key is stored in the `config.json` file."), 66 | "civsfz_request_timeout": shared.OptionInfo( 67 | 30, 68 | label="Request timeout(s)", 69 | component=gr.Slider, 70 | component_args={"minimum": 10, "maximum": 90, "step": 5}, 71 | ), 72 | "civsfz_browsing_level": shared.OptionInfo( 73 | [1], 74 | label="Browsing level", 75 | component=gr.CheckboxGroup, 76 | component_args={ 77 | "choices": list(Api.nsfwLevel.items()), 78 | }, 79 | ), 80 | "civsfz_number_of_tabs": shared.OptionInfo( 81 | 3, 82 | label="Number of tabs", 83 | component=gr.Slider, 84 | component_args={"minimum": 1, "maximum": 8, "step": 1}, 85 | ).needs_reload_ui(), 86 | "civsfz_number_of_cards": shared.OptionInfo( 87 | 12, 88 | label="Number of cards per page", 89 | component=gr.Slider, 90 | component_args={"minimum": 8, "maximum": 48, "step": 1}, 91 | ), 92 | "civsfz_card_size_width": shared.OptionInfo( 93 | 8, 94 | label="Card width (unit:1em)", 95 | component=gr.Slider, 96 | component_args={"minimum": 5, "maximum": 30, "step": 1}, 97 | ), 98 | "civsfz_card_size_height": shared.OptionInfo( 99 | 12, 100 | label="Card height (unit:1em)", 101 | component=gr.Slider, 102 | component_args={"minimum": 5, "maximum": 30, "step": 1}, 103 | ), 104 | "civsfz_hover_zoom_magnification": shared.OptionInfo( 105 | 1.5, 106 | label="Zoom magnification when hovering", 107 | component=gr.Slider, 108 | component_args={"minimum": 1, "maximum": 2.4, "step": 0.1}, 109 | ), 110 | "civsfz_treat_x_as_nsfw": shared.OptionInfo( 111 | True, 112 | label='If the first image is of type "X", treat the model as nsfw', 113 | component=gr.Checkbox, 114 | ), 115 | "civsfz_treat_slash_as_folder_separator": shared.OptionInfo( 116 | False, 117 | label=r'Treat "/" as folder separator. If you change this, some models may not be able to confirm the existence of the file.', 118 | component=gr.Checkbox, 119 | ), 120 | "civsfz_discard_different_hash": shared.OptionInfo( 121 | True, 122 | label="Discard downloaded model file if the hash value differs", 123 | component=gr.Checkbox, 124 | ), 125 | "civsfz_overwrite_metadata_file": shared.OptionInfo( 126 | False, 127 | label="Allow metadata files to be overwritten. (xxx.txt/xxx.json)", 128 | component=gr.Checkbox, 129 | ), 130 | "civsfz_length_of_conditions_history": shared.OptionInfo( 131 | 5, 132 | label="Length of conditions history", 133 | component=gr.Slider, 134 | component_args={"minimum": 5, "maximum": 10, "step": 1}, 135 | ), 136 | "civsfz_length_of_search_history": shared.OptionInfo( 137 | 5, 138 | label="Length of search term history", 139 | component=gr.Slider, 140 | component_args={"minimum": 5, "maximum": 30, "step": 1}, 141 | ), 142 | } 143 | 144 | dict_background_opacity = { 145 | "civsfz_background_opacity": shared.OptionInfo( 146 | 0.75, 147 | label="Background opacity for model name", 148 | component=gr.Slider, 149 | component_args={"minimum": 0.0, "maximum": 1.0, "step": 0.05}, 150 | ), 151 | } 152 | 153 | dict_shadow_color = { 154 | "civsfz_shadow_color_default": shared.OptionInfo( 155 | "#798a9f", 156 | label="Frame color for cards", 157 | component=gr.ColorPicker, 158 | ), 159 | "civsfz_shadow_color_alreadyhave": shared.OptionInfo( 160 | "#7fffd4", 161 | label="Frame color for cards you already have", 162 | component=gr.ColorPicker, 163 | ), 164 | "civsfz_shadow_color_alreadyhad": shared.OptionInfo( 165 | "#caff7f", 166 | label="Frame color for cards with updates", 167 | component=gr.ColorPicker, 168 | ), 169 | } 170 | dict_folder_info = ( 171 | { 172 | "civsfz_msg_html2": shared.OptionHTML( 173 | "

How to set subfolders

" 174 | "Click here(Wiki|GitHub) to learn how to specify the model folder and subfolders." 175 | ), 176 | } 177 | if not platform == "SD.Next" 178 | else {} 179 | ) 180 | 181 | label = 'Save folders for Types. Set JSON Key-Value pair. The folder separator is "/" not "\\". The path is relative from models folder or absolute.' 182 | info = 'Example for Stability Matrix: {"Checkpoint":"Stable-diffusion/sd"}' 183 | placefolder = '{\n\ 184 | "Checkpoint": "MY_SUBFOLDER",\n\ 185 | "VAE": "",\n\ 186 | "TextualInversion": "",\n\ 187 | "LORA": "",\n\ 188 | "LoCon": "",\n\ 189 | "Hypernetwork": "",\n\ 190 | "AestheticGradient": "",\n\ 191 | "Controlnet": "ControlNet",\n\ 192 | "Upscaler": "OtherModels/Upscaler",\n\ 193 | "MotionModule": "OtherModels/MotionModule",\n\ 194 | "Poses": "OtherModels/Poses",\n\ 195 | "Wildcards": "OtherModels/Wildcards",\n\ 196 | "Workflows": "OtherModels/Workflows",\n\ 197 | "Other": "OtherModels/Other"\n\ 198 | }' 199 | 200 | dict_folders = { 201 | "civsfz_save_type_folders": ( 202 | shared.OptionInfo( 203 | "", 204 | label=label + " " + info, 205 | comment_after=None, 206 | component=gr.Code, 207 | component_args=lambda: { 208 | "language": "python", 209 | "interactive": True, 210 | "lines": 4, 211 | }, 212 | ) 213 | if GR_V440 214 | else shared.OptionInfo( 215 | "", 216 | label=label, 217 | component=gr.Textbox, 218 | component_args={ 219 | "lines": 4, 220 | "info": info, 221 | "placeholder": placefolder, 222 | }, 223 | ) 224 | ), 225 | "civsfz_save_subfolder": shared.OptionInfo( 226 | "", 227 | label='Subfolders under type folders. Model information can be referenced by the key name enclosed in double curly brachets "{{}}". Available key names are "BASEMODELbkCmpt", "BASEMODEL", "NSFW", "USERNAME", "MODELNAME", "MODELID", "VERSIONNAME" and "VERSIONID". Folder separator is "/".', 228 | component=gr.Textbox, 229 | component_args={ 230 | "lines": 1, 231 | "placeholder": "_{{BASEMODEL}}/.{{NSFW}}/{{MODELNAME}}", 232 | }, 233 | ), 234 | } 235 | dict_color_figcaption = { 236 | "civsfz_background_color_figcaption": shared.OptionInfo( 237 | "#414758", 238 | label="Background color for model name", 239 | component=gr.ColorPicker, 240 | ), 241 | } 242 | 243 | dict_color_family = ( 244 | { 245 | "civsfz_msg_html3": shared.OptionHTML( 246 | "

What is Family?

" 247 | "Families are groups of similar base models. " 248 | "You can register the base model to the family. " 249 | "You can set the color for each color family. " 250 | "Colors within a family will automatically change based on the family color. " 251 | "The color changes gradually according to the hls color wheel." 252 | ), 253 | } 254 | if not platform == "SD.Next" 255 | else {} 256 | ) 257 | for k in familyColor.keys(): 258 | if k not in "non_family": 259 | dict_color_family |= { 260 | "civsfz_" 261 | + k: shared.OptionInfo( 262 | familyColor[k]["value"], 263 | label=k.capitalize(), 264 | component=ui_components.DropdownMulti, # Prevent multiselect error on A1111 v1.10.0 265 | component_args={ 266 | "choices": Api.getBasemodelOptions(), 267 | }, 268 | ), 269 | "civsfz_color_" 270 | + k: shared.OptionInfo( 271 | familyColor[k]["color"], 272 | label=k.capitalize() + " color", 273 | component=gr.ColorPicker, 274 | ).js("preview", "civsfz_preview_colors"), 275 | } 276 | else: 277 | dict_color_family |= { 278 | "civsfz_color_" 279 | + k: shared.OptionInfo( 280 | familyColor[k]["color"], 281 | label="Non-family color", 282 | component=gr.ColorPicker, 283 | ), 284 | } 285 | 286 | for key, opt in { 287 | **dict_user_management, 288 | **dict_api_info, 289 | **dict_options1, 290 | **dict_background_opacity, 291 | **dict_color_figcaption, 292 | **dict_color_family, 293 | **dict_shadow_color, 294 | **dict_folder_info, 295 | **dict_folders, 296 | }.items(): 297 | opt.section = civsfz_section 298 | shared.opts.add_option(key, opt) 299 | 300 | 301 | script_callbacks.on_ui_settings(on_ui_settings) 302 | -------------------------------------------------------------------------------- /scripts/civsfz_downloader.py: -------------------------------------------------------------------------------- 1 | import gradio as gr 2 | import math 3 | import os 4 | import re 5 | import requests 6 | from colorama import Fore, Back, Style 7 | from collections import deque 8 | from datetime import datetime, timedelta, timezone 9 | from jinja2 import Environment, FileSystemLoader 10 | from pathlib import Path 11 | from threading import Thread, local 12 | from time import sleep 13 | from tqdm import tqdm 14 | from scripts.civsfz_shared import opts, calculate_sha256, read_timeout 15 | from scripts.civsfz_filemanage import ( 16 | makedirs, 17 | removeFile, 18 | extensionFolder, 19 | open_folder, 20 | filename_normalization, 21 | ) 22 | 23 | def print_ly(x): return print(Fore.LIGHTYELLOW_EX + 24 | "CivBrowser: " + x + Style.RESET_ALL) 25 | def print_lc(x): return print(Fore.LIGHTCYAN_EX + 26 | "CivBrowser: " + x + Style.RESET_ALL) 27 | def print_n(x): return print("CivBrowser: " + x) 28 | 29 | class Downloader: 30 | _dlQ = deque() # Download 31 | _threadQ = deque() # Downloading 32 | _ctrlQ = deque() # control 33 | # _msgQ = deque() # msg from worker 34 | _thread_local = local() 35 | _dlResults = deque() # Download results 36 | _maxThreadNum = 2 37 | _threadNum = 0 38 | 39 | def __init__(self) -> None: 40 | Downloader._thread_local.progress = 0 41 | 42 | def get_session(self) -> requests.Session: 43 | if not hasattr(Downloader._thread_local, 'session'): 44 | Downloader._thread_local.session = requests.Session() 45 | return Downloader._thread_local.session 46 | 47 | def add(self, folder, filename, url, hash, api_key, early_access): 48 | filename = filename_normalization(filename) 49 | if Downloader._threadNum == 0: 50 | # Clear queue because garbage may remain due to errors that cannot be caught 51 | Downloader._threadQ.clear() 52 | path = Path(folder, filename) 53 | Downloader._ctrlQ.clear() # Clear cancel request 54 | if (not any(item['path'] == path for item in Downloader._dlQ) 55 | and not any(item['path'] == path for item in Downloader._threadQ)): 56 | Downloader._dlQ.append({"folder": folder, 57 | "filename": filename, 58 | "path": path, 59 | "url": url, 60 | "hash": hash, 61 | "apiKey": api_key, 62 | "EarlyAccess": early_access 63 | }) 64 | if Downloader._threadNum < Downloader._maxThreadNum: 65 | Downloader._threadNum += 1 66 | worker = Thread(target=self.download) 67 | worker.start() 68 | else: 69 | return "Already in queue" 70 | return f"Queue {len(Downloader._dlQ)}: Threads {Downloader._threadNum}" 71 | 72 | def sendCancel(self, path:Path): 73 | ''' 74 | Cancel downloading by file path 75 | ''' 76 | # path = Path(folder, filename) 77 | delete = None 78 | for dl in Downloader._dlQ: 79 | if path == dl["path"]: 80 | delete = dl 81 | break 82 | if delete is None: 83 | Downloader._ctrlQ.append({"control": "cancel", "path": path}) 84 | else: 85 | Downloader._dlQ.remove(dl) 86 | print_lc(f"Canceled:{path}") 87 | return f"Canceled:{path}" 88 | 89 | def status(self): 90 | now = datetime.now(timezone.utc) 91 | # Discard past results 92 | expireQ = deque() 93 | remove = None 94 | deadline = 3 * 60 95 | for item in Downloader._dlResults: 96 | tdDiff = now - item['completedAt'] 97 | secDiff = math.ceil(abs(tdDiff / timedelta(seconds=1))) 98 | item['expiration'] = secDiff / deadline 99 | if secDiff < deadline: 100 | expireQ.append(item) 101 | else: 102 | remove = item 103 | if remove is not None: 104 | Downloader._dlResults.remove(remove) 105 | 106 | templatesPath = Path.joinpath( 107 | extensionFolder(), Path("../templates")) 108 | environment = Environment( 109 | loader=FileSystemLoader(templatesPath.resolve()), 110 | extensions=["jinja2.ext.loopcontrols"], 111 | ) 112 | template = environment.get_template("downloadQueue.jinja") 113 | content = template.render( 114 | threadQ=Downloader._threadQ, waitQ=Downloader._dlQ, resultQ=expireQ) 115 | return content 116 | 117 | def dlHtml(self): 118 | html = self.status() 119 | return html 120 | def uiDlList(self, gr:gr, every:float=None): 121 | grHtmlDlQueue = gr.HTML(elem_id=f"civsfz_download_queue", value=lambda: self.dlHtml(), every=every) 122 | return grHtmlDlQueue 123 | 124 | def uiJsEvent(self, gr: gr): 125 | # Cancel Download item 126 | grTxtJsEventDl = gr.Textbox( 127 | label="Event text", 128 | value=None, 129 | elem_id="civsfz_eventtext_dl", 130 | visible=False, 131 | interactive=True, 132 | lines=1, 133 | ) 134 | def eventDl(grTxtJsEventDl): 135 | command = grTxtJsEventDl.split("??") 136 | if command[0].startswith("CancelDl"): 137 | path = Path(command[1]) 138 | self.sendCancel(path) 139 | gr.Info(f"Cancel") 140 | elif command[0].startswith("OpenFolder"): 141 | path = Path(command[1]) 142 | open_folder(path) 143 | gr.Info(f"Open folder") 144 | return 145 | 146 | grTxtJsEventDl.change( 147 | fn=eventDl, 148 | inputs=[grTxtJsEventDl], 149 | outputs=[], 150 | ) 151 | 152 | def download(self) -> None: 153 | session = self.get_session() 154 | result = "" # Success or Error 155 | while len(Downloader._dlQ) > 0: 156 | q = Downloader._dlQ.popleft() 157 | q['progress'] = 0 # add progress info 158 | Downloader._threadQ.append(q) # for download list 159 | url = q["url"] 160 | folder = q["folder"] 161 | filename = q["filename"] 162 | hash = q["hash"] 163 | api_key = q["apiKey"] 164 | early_access = q["EarlyAccess"] 165 | # 166 | makedirs(folder) 167 | file_name = os.path.join(folder, filename) 168 | # Maximum number of retries 169 | max_retries = 3 170 | # Delay between retries (in seconds) 171 | retry_delay = 3 172 | 173 | downloaded_size = 0 174 | headers = { 175 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0' 176 | } 177 | applyAPI = False # True if API key is added 178 | mode = "wb" # Open file mode 179 | if os.path.exists(file_name): 180 | print_lc("Overwrite") 181 | 182 | # Split filename from included path 183 | tokens = re.split(re.escape('\\'), file_name) 184 | file_name_display = tokens[-1] 185 | cancel = False 186 | exitDownloading = False 187 | while not exitDownloading: 188 | # Send a GET request to the URL and save the response to the local file 189 | try: 190 | # Get the total size of the file 191 | with session.get( 192 | url, 193 | headers=headers, 194 | stream=True, 195 | timeout=read_timeout(), 196 | ) as response: 197 | response.raise_for_status() 198 | # print_lc(f"{response.headers=}") 199 | if 'Content-Length' in response.headers: 200 | total_size = int( 201 | response.headers.get("Content-Length", 0)) 202 | # Update the total size of the progress bar if the `Content-Length` header is present 203 | if total_size == 0: 204 | total_size = downloaded_size 205 | with tqdm(total=1000000000, unit="B", unit_scale=True, desc=f"Downloading {file_name_display}", initial=downloaded_size, leave=True) as progressConsole: 206 | prg = 0 # downloaded_size 207 | progressConsole.total = total_size 208 | with open(file_name, mode) as file: 209 | for chunk in response.iter_content(chunk_size=1*1024*1024): 210 | if chunk: # filter out keep-alive new chunks 211 | file.write(chunk) 212 | progressConsole.update(len(chunk)) 213 | prg += len(chunk) 214 | # update progress 215 | i = Downloader._threadQ.index(q) 216 | q['progress'] = prg / total_size 217 | Downloader._threadQ[i] = q 218 | # cancel 219 | if len(Downloader._ctrlQ) > 0: 220 | ctrl = Downloader._ctrlQ[0] 221 | if ctrl["control"] == "cancel" and Path(folder, filename) == Path(ctrl["path"]) : 222 | Downloader._ctrlQ.popleft() 223 | exitDownloading = True 224 | cancel = True 225 | result = "Canceled" 226 | print_lc( 227 | f"Canceled:{file_name_display}:") 228 | break 229 | downloaded_size = os.path.getsize(file_name) 230 | # Break out of the loop if the download is successful 231 | break 232 | else: 233 | # if early_access and applyAPI: 234 | # print_ly( 235 | # f"{file_name_display}:Download canceled. Early Access!") 236 | # exitDownloading = True 237 | # result = "Early Access" 238 | # break 239 | if not applyAPI: 240 | print_lc("May need API key") 241 | if len(api_key) == 32: 242 | headers.update( 243 | {"Authorization": f"Bearer {api_key}"}) 244 | applyAPI = True 245 | print_lc(f"{file_name_display}:Apply API key") 246 | else: 247 | exitDownloading = True 248 | result = "No API key" 249 | break 250 | else: 251 | exitDownloading = True 252 | print_lc( 253 | f"{file_name_display}:Invalid API key or Early Access" 254 | ) 255 | result = "Invalid API key or Early Access" 256 | break 257 | 258 | except requests.exceptions.Timeout as e: 259 | print_ly(f"{file_name_display}:{e}") 260 | result = "Timeout" 261 | except ConnectionError as e: 262 | print_ly(f"{file_name_display}:{e}") 263 | result = "Connection Error" 264 | except requests.exceptions.RequestException as e: 265 | print_ly(f"{file_name_display}:{e}") 266 | result = "Request exception" 267 | except Exception as e: 268 | print_ly(f"{file_name_display}:{e}") 269 | result = "Exception" 270 | exitDownloading = True 271 | # Decrement the number of retries 272 | max_retries -= 1 273 | # If there are no more retries, raise the exception 274 | if max_retries == 0: 275 | exitDownloading = True 276 | result += "(Max retry failure)" 277 | break 278 | # Wait for the specified delay before retrying 279 | print_lc(f"{file_name_display}:Retry wait {retry_delay}s") 280 | sleep(retry_delay) 281 | 282 | if exitDownloading: 283 | if cancel: 284 | print_lc(f'Canceled : {file_name_display}') 285 | # gr.Warning(f"Canceled: {file_name_display}") 286 | if os.path.exists(file_name): 287 | removeFile(file_name) 288 | else: 289 | if os.path.exists(file_name): 290 | # downloaded_size = os.path.getsize(file_name) 291 | # Check if the download was successful 292 | sha256 = calculate_sha256(file_name).upper() 293 | print_lc(f'Downloaded hash : {sha256}') 294 | if hash != "": 295 | if sha256[:len(hash)] == hash.upper(): 296 | print_n(f"Save: {file_name_display}") 297 | result = "Succeeded" 298 | # gr.Info(f"Success: {file_name_display}") 299 | else: 300 | print_lc(f'Model file hash : {hash}') 301 | print_ly(f"Hash mismatch. {file_name_display}") 302 | # gr.Warning(f"Hash mismatch: {file_name_display}") 303 | if opts.civsfz_discard_different_hash: 304 | removeFile(file_name) 305 | else: 306 | print_ly("Not trashed due to your setting.") 307 | result = "Hash mismatch" 308 | else: 309 | print_n(f"Save: {file_name_display}") 310 | print_ly("No hash value provided. Unable to confirm file.") 311 | result = "No hash value" 312 | # gr.Info(f"No hash: {file_name_display}") 313 | # Downloader._dlQ.task_done() 314 | Downloader._threadQ.remove(q) 315 | q['result'] = result 316 | q['completedAt'] = datetime.now(timezone.utc) 317 | Downloader._dlResults.append(q) 318 | Downloader._threadNum -= 1 319 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sd-civitai-browser (CivBrowser) 2 | 3 | An extension to help download models from CivitAi without leaving WebUI 4 | 5 | ## Modifications 6 | 7 | 8 | 9 | ***If you fork, please replace prefix `civsfz` in file names, function names, css class names, etc. with your own prefix to avoid conflicts.*** 10 | 11 | ## Features 12 | 13 | - Works with ***A1111***, ***Forge***, ***SD.Next***, ***Forge Neo***(experimental)(above v2.3.5) 14 | - SD.Next is far away from A1111, so it may stop working eventually 15 | - Search Civitai models in ***multiple tabs*** 16 | - Download ***queue*** and ***multithreaded*** downloads 17 | - List models as ***card image*** 18 | - Safe display in ***sfw*** search 19 | - Highlight models ***you have*** 20 | - Highlight models that ***can be updated*** 21 | - Searchable by specifying ***Base Models*** 22 | - Show ***Base Model*** on a model card 23 | - Display/***save*** model information ***in HTML*** 24 | - ***Json data*** of model is also ***saved*** in the same folder as the model file 25 | - If the sample image has meta data, display it with ***infotext compatibility*** 26 | - Click on the image to ***send to txt2img*** 27 | - Check downloaded model file with ***SHA256*** 28 | - ***Automatically*** set save folder, or specify it directly 29 | - Card ***size and colors*** can be changed in Settings 30 | - Support ***API Key*** 31 | - ~~Show the ***Early Access*** period of models~~ 32 | - Ban creators feature (Hide models by creator names) 33 | - Favorite creators feature (Highlight the model with ⭐️) 34 | - Stores metadata. 35 | - Create description and activation text from model information. 36 | - If metadata already exists, it will not be overwritten. Can be changed in settings. 37 | 38 | ## Installation 39 | 40 | 1. Launch SD-webUI. 41 | 2. Open tab `Extensions` on SD-webUI. 42 | 3. Open tab `Available`. 43 | 4. Click `Load from:` button. 44 | 5. Search `civbrowser`. 45 | 6. Click `install` button. 46 | 7. Wait for the installation to complete. 47 | 8. Open tab `Installed`. 48 | 9. Click `Apply and quit` button. 49 | 10. Restart SD-webUI. 50 | 51 | ## Versions 52 | 53 | ### v2.9 54 | 55 | - Add support for Forge Neo (experimental) 56 | - Video models are treated the same as SD models. 57 | - The compatibility with ComfyUI, such as unet and diffusion_models folders, is undecided. 58 | - Separate folders are required for gguf and safetensors, but this cannot be determined due to insufficient model information. 59 | 60 | ### v2.8 61 | 62 | - Stores metadata. 63 | - Create description and activation text from model information. 64 | - If metadata already exists, it will not be overwritten. Can be changed in settings. 65 | - You can now search using keywords, usernames, and tags at the same time. 66 | - Searching for Favorites on Civitai. 67 | - Some model information can now be collapsed and hidden. Works in modern browsers. 68 | 69 | ### v2.7 70 | 71 | - Update of option acquisitions due to API response change. 72 | 73 | #### v2.7.5 74 | 75 | - Support for changing search response by model name. Pageneation is back. 76 | 77 | #### v2.7.4 78 | 79 | - The maximum number of cards per page when searching model names is now 100. There is no second page because the API response does not include pagination. This is a temporary change. 80 | 81 | #### [reference](https://github.com/civitai/civitai/blob/main/src/components/PermissionIndicator/PermissionIndicator.tsx#L15) 377 | - Avoid collision when there are same model names 378 | - Deprecate new folder checkbox and its function 379 | - Change version selection from dropdown to radio button 380 | 381 | ### v1.1.0 and before 382 | 383 | - Apply changes made by [thetrebor](https://github.com/thetrebor/sd-civitai-browser) 384 | - Support LoRA 385 | - Set folders from cmd_opts 386 | - Save HTML with images 387 | - Add Trained Tags in html 388 | - Add meta data in html 389 | - Copy first image as thumbnail 390 | - Add thumbnail preview list 391 | - Save model data as ".civitai.info" 392 | - Support LoCon/LyCORIS 393 | - Press the Download Model button again to cancel the download 394 | - Add previous button 395 | - Click on a thumbnail to select a model 396 | - Add zoom effect to thumbnails 397 | - Support ControlNet/Poses 398 | - Support user and tag name search 399 | - Implement page controls 400 | - Support for `--lyco-dir`(To use the existing _LoCon folder, specify with `--lyco-dir`) 401 | - Change the color of the frame of the card that has already been downloaded 402 | - For base models other than SD1, save to a subfolder of the base model name 403 | - Support `--lyco-dir-backcompat` modified in v1.5.1 of SD web UI 404 | - Save to subfolder of base model name (e.g. `_SDXL_1_0` , `_Other`) 405 | -------------------------------------------------------------------------------- /scripts/civsfz_filemanage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import urllib.parse 4 | import shutil 5 | import json 6 | import re 7 | from pathlib import Path 8 | import platform 9 | import subprocess as sp 10 | from collections import deque 11 | from modules import sd_models 12 | from colorama import Fore, Back, Style 13 | from scripts.civsfz_shared import cmd_opts, opts, read_timeout 14 | from modules.paths import models_path 15 | try: 16 | from send2trash import send2trash 17 | send2trash_installed = True 18 | except ImportError: 19 | print("Recycle bin cannot be used.") 20 | send2trash_installed = False 21 | 22 | print_ly = lambda x: print(Fore.LIGHTYELLOW_EX + "CivBrowser: " + x + Style.RESET_ALL ) 23 | print_lc = lambda x: print(Fore.LIGHTCYAN_EX + "CivBrowser: " + x + Style.RESET_ALL ) 24 | print_n = lambda x: print("CivBrowser: " + x ) 25 | 26 | isDownloading = False 27 | ckpt_dir = cmd_opts.ckpt_dir or sd_models.model_path 28 | pre_opt_folder = None 29 | 30 | def extensionFolder() -> Path: 31 | return Path(__file__).parent 32 | 33 | def name_len(s:str): 34 | #return len(s.encode('utf-16')) 35 | return len(s.encode('utf-8')) 36 | def cut_name(s:str): 37 | MAX_FILENAME_LENGTH = 246 38 | l = name_len(s) 39 | #print_lc(f'filename length:{len(s.encode("utf-8"))}-{len(s.encode("utf-16"))}') 40 | while l >= MAX_FILENAME_LENGTH: 41 | s = s[:-1] 42 | l = name_len(s) 43 | return s 44 | 45 | 46 | def escaped_filename(model_name): 47 | escapechars = str.maketrans({ " ": r"_", 48 | "(": r"", 49 | ")": r"", 50 | "|": r"", 51 | ":": r"", 52 | ",": r"_", 53 | "<": r"", 54 | ">": r"", 55 | "!": r"", 56 | "?": r"", 57 | ".": r"_", 58 | "&": r"_and_", 59 | "*": r"_", 60 | "\"": r"", 61 | "\\": r"", 62 | "\t":r"_", 63 | "/": r"/" if opts.civsfz_treat_slash_as_folder_separator else r"_" 64 | }) 65 | new_name = model_name.translate(escapechars) 66 | new_name = re.sub('_+', '_', new_name) 67 | new_name = cut_name(new_name) 68 | return new_name 69 | 70 | 71 | def type_path(type: str) -> Path: 72 | global pre_opt_folder, ckpt_dir 73 | if opts.civsfz_save_type_folders != "": 74 | try: 75 | folderSetting = json.loads(opts.civsfz_save_type_folders) 76 | except json.JSONDecodeError as e: 77 | if pre_opt_folder != opts.civsfz_save_type_folders: 78 | print_ly(f'Check subfolder setting: {e}') 79 | folderSetting = {} 80 | else: 81 | folderSetting = {} 82 | pre_opt_folder = opts.civsfz_save_type_folders 83 | base = models_path 84 | if type == "Checkpoint": 85 | default = ckpt_dir 86 | # folder = ckpt_dir 87 | folder = os.path.relpath(default, base) # os.path.join(models_path, "Stable-diffusion") 88 | elif type == "Hypernetwork": 89 | default = cmd_opts.hypernetwork_dir 90 | folder = os.path.relpath(default, base) 91 | elif type == "TextualInversion": 92 | default = cmd_opts.embeddings_dir 93 | folder = os.path.relpath(default, base) 94 | elif type == "AestheticGradient": 95 | default = os.path.join(models_path, "../extensions/stable-diffusion-webui-aesthetic-gradients/aesthetic_embeddings") 96 | folder = os.path.relpath(default, base) 97 | elif type == "LORA": 98 | default = cmd_opts.lora_dir # "models/Lora" 99 | folder = os.path.relpath(default, base) 100 | elif type == "LoCon": 101 | #if "lyco_dir" in cmd_opts: 102 | # default = f"{cmd_opts.lyco_dir}" 103 | #elif "lyco_dir_backcompat" in cmd_opts: # A1111 V1.5.1 104 | # default = f"{cmd_opts.lyco_dir_backcompat}" 105 | #else: 106 | default = os.path.join(models_path, "Lora/_LyCORIS") 107 | folder = os.path.relpath(default, base) 108 | elif type == "DoRA": 109 | default = os.path.join(models_path, "Lora/_DoRA") # "models/Lora/_DoRA" 110 | folder = os.path.relpath(default, base) 111 | elif type == "VAE": 112 | if cmd_opts.vae_dir: 113 | default = cmd_opts.vae_dir # "models/VAE" 114 | else: 115 | default = os.path.join(models_path, "VAE") 116 | folder = os.path.relpath(default, base) 117 | elif type == "Controlnet": 118 | folder = "ControlNet" 119 | elif type == "Poses": 120 | folder = "OtherModels/Poses" 121 | elif type == "Upscaler": 122 | folder = "OtherModels/Upscaler" 123 | elif type == "MotionModule": 124 | folder = "OtherModels/MotionModule" 125 | elif type == "Wildcards": 126 | folder = "OtherModels/Wildcards" 127 | elif type == "Workflows": 128 | folder = "OtherModels/Workflows" 129 | elif type == "Detection": 130 | folder = "OtherModels/Detection" 131 | elif type == "Other": 132 | folder = "OtherModels/Other" 133 | 134 | optFolder = folderSetting.get(type, "") 135 | if optFolder == "": 136 | pType = Path(base) / Path(folder) 137 | else: 138 | pType = Path(base) / Path(optFolder) 139 | return pType 140 | 141 | def basemodel_path(baseModel: str) -> Path: 142 | basemodelPath = "" 143 | if not 'SD 1' in baseModel: 144 | basemodelPath = '_' + baseModel.replace(' ', '_').replace('.', '_') 145 | return Path(basemodelPath) 146 | 147 | def basemodel_path_all(baseModel: str) -> Path: 148 | basemodelPath = baseModel.replace(' ', '_').replace('.', '_') 149 | return Path(basemodelPath) 150 | 151 | def generate_model_save_path2(type, modelName: str = "", baseModel: str = "", nsfw: bool = False, userName=None, mID=None, vID=None, versionName=None) -> Path: 152 | # TYPE, MODELNAME, BASEMODEL, NSFW, UPNAME, MODEL_ID, VERSION_ID 153 | subfolders = { 154 | # "TYPE": type_path(type).as_posix(), 155 | "BASEMODELbkCmpt": basemodel_path(baseModel).as_posix(), 156 | "BASEMODEL": basemodel_path_all(baseModel).as_posix(), 157 | "NSFW": "nsfw" if nsfw else None, 158 | "USERNAME": escaped_filename(userName) if userName is not None else None, 159 | "MODELNAME": escaped_filename(modelName), 160 | "MODELID": str(mID) if mID is not None else None, 161 | "VERSIONNAME": escaped_filename(versionName), 162 | "VERSIONID": str(vID) if vID is not None else None, 163 | } 164 | if not str.strip(opts.civsfz_save_subfolder): 165 | subTree = "_{{BASEMODEL}}/.{{NSFW}}/{{MODELNAME}}" 166 | else: 167 | subTree = str.strip(opts.civsfz_save_subfolder) 168 | subTreeList = subTree.split("/") 169 | newTreeList = [] 170 | for i, sub in enumerate(subTreeList): 171 | if sub: 172 | subKeys = re.findall("(\{\{(.+?)\}\})", sub) 173 | newSub = "" 174 | replaceSub = sub 175 | for subKey in subKeys: 176 | if subKey[1] is not None: 177 | if subKey[1] in subfolders: 178 | folder = subfolders[subKey[1]] 179 | if folder is not None: 180 | replaceSub = re.sub( 181 | "\{\{" + subKey[1] + "\}\}", folder, replaceSub) 182 | else: 183 | replaceSub = re.sub( 184 | "\{\{" + subKey[1] + "\}\}", "", replaceSub) 185 | else: 186 | print_ly(f'"{subKey[1]}" is not defined') 187 | replaceSub = "ERROR" 188 | newSub += replaceSub if replaceSub is not None else "" 189 | 190 | if newSub != "": 191 | newTreeList.append(newSub) 192 | else: 193 | if i != 0: 194 | print_lc(f"Empty subfolder:{i}") 195 | modelPath = type_path(type).joinpath( 196 | "/".join(newTreeList)) 197 | return modelPath 198 | 199 | def filename_normalization(filename) -> str: 200 | if filename: 201 | filename = re.sub(r"__+", "_", filename) 202 | return filename 203 | 204 | 205 | def save_text_file(folder, filename, trained_words, description:str=""): 206 | filename = filename_normalization(filename) 207 | makedirs(folder) 208 | # filepath = os.path.join(folder, filename.replace(".ckpt",".txt")\ 209 | # .replace(".safetensors",".txt")\ 210 | # .replace(".pt",".txt")\ 211 | # .replace(".yaml",".txt")\ 212 | # .replace(".zip",".txt")\ 213 | # ) 214 | filepath = Path(folder) / Path(filename) 215 | filepath = filepath.with_suffix(".txt") 216 | overwrite = opts.civsfz_overwrite_metadata_file 217 | if overwrite: 218 | print_n(f"Overwrite allowed in settings") 219 | else: 220 | print_n(f"Overwrite not allowed in settings") 221 | if not filepath.exists() or overwrite: 222 | with open(filepath, 'w', encoding='UTF-8') as f: 223 | f.write(trained_words) 224 | print_n(f"Save {filepath.name}") 225 | else: 226 | print_n(f"File exists, so dosen't save {filepath.name}") 227 | 228 | # Save trigger words as metadata 229 | filepath = filepath.with_suffix(".json") 230 | metadata = {} 231 | try: 232 | if filepath.exists(): 233 | with open(filepath, "r", encoding="utf8") as f: 234 | metadata = json.load(f) 235 | except Exception as e: 236 | print_ly(f"reading metadata from {filepath}:{e}" ) 237 | # print_lc(f"{metadata=}") 238 | metadata["description"] = description 239 | metadata["activation text"] = re.sub(r"<.+?>", "", trained_words) # delete "" 240 | if not filepath.exists() or overwrite: 241 | try: 242 | with open(filepath, "w", encoding="UTF-8") as f: 243 | json.dump(metadata, f) 244 | print_n(f"Save {filepath.name}") 245 | except Exception as e: 246 | print_ly(f"Writing metadata to {filepath}:{e}") 247 | else: 248 | print_n(f"File exists, so dosen't save {filepath.name}") 249 | return "Save text" 250 | 251 | 252 | def makedirs(folder): 253 | if not os.path.exists(folder): 254 | os.makedirs(folder) 255 | print_lc(f'Make folder: {folder}') 256 | 257 | def isExistFile(folder, file): 258 | file = filename_normalization(file) 259 | isExist = False 260 | if folder != "" and folder is not None: 261 | path = os.path.join(folder, file) 262 | isExist = os.path.exists(path) 263 | return isExist 264 | 265 | 266 | def saveImageFiles(folder, versionName, html, content_type, versionInfo): 267 | html = versionInfo['html0'] 268 | makedirs(folder) 269 | img_urls = re.findall(r'src=[\'"]?([^\'" >]+)', html) 270 | basename = os.path.splitext(versionName)[0] # remove extension 271 | basename = filename_normalization(basename) 272 | preview_url = versionInfo["modelVersions"][0]["images"][0]["url"] 273 | preview_url = urllib.parse.quote(preview_url, safe=':/=') 274 | if 'images' in versionInfo: 275 | for img in versionInfo['images']: 276 | if img['type'] == 'image': 277 | preview_url = img['url'] 278 | preview_url = urllib.parse.quote(preview_url, safe=':/=') 279 | break 280 | with requests.Session() as session: 281 | HTML = html 282 | for i, img_url in enumerate(img_urls): 283 | isVideo = False 284 | filename = "" 285 | filenamethumb = "" 286 | for img in versionInfo["modelVersions"][0]["images"]: 287 | if img['url'] == img_url: 288 | if img['type'] == 'video': 289 | isVideo = True 290 | # print(Fore.LIGHTYELLOW_EX + f'URL: {img_url}'+ Style.RESET_ALL) 291 | if isVideo: 292 | if content_type == "TextualInversion": 293 | filename = f'{basename}_{i}.preview.webm' 294 | filenamethumb = f'{basename}.preview.webm' 295 | else: 296 | filename = f'{basename}_{i}.webm' 297 | filenamethumb = f'{basename}.webm' 298 | else: 299 | if content_type == "TextualInversion": 300 | filename = f'{basename}_{i}.preview.png' 301 | filenamethumb = f'{basename}.preview.png' 302 | else: 303 | filename = f'{basename}_{i}.png' 304 | filenamethumb = f'{basename}.png' 305 | 306 | HTML = HTML.replace(img_url, f'"{filename}"') 307 | url_parse = urllib.parse.urlparse(img_url) 308 | if url_parse.scheme: 309 | # img_url.replace("https", "http").replace("=","%3D") 310 | img_url = urllib.parse.quote(img_url, safe=':/=') 311 | try: 312 | response = session.get(img_url, timeout=read_timeout()) 313 | with open(os.path.join(folder, filename), 'wb') as f: 314 | f.write(response.content) 315 | if img_url == preview_url: 316 | shutil.copy2(os.path.join(folder, filename), 317 | os.path.join(folder, filenamethumb)) 318 | print_n(f"Save {filename}") 319 | # with urllib.request.urlretrieve(img_url, os.path.join(model_folder, filename)) as dl: 320 | except requests.exceptions.Timeout as e: 321 | print_ly(f'Error: {e}') 322 | print_ly(f'URL: {img_url}') 323 | # return "Err: Save infos" 324 | except requests.exceptions.RequestException as e: 325 | print_ly(f'Error: {e}') 326 | print_ly(f'URL: {img_url}') 327 | filepath = os.path.join(folder, f'{basename}.html') 328 | with open(filepath, 'wb') as f: 329 | f.write(HTML.encode('utf8')) 330 | print_n(f"Save {basename}.html") 331 | # Save json_info 332 | filepath = os.path.join(folder, f'{basename}.civitai.json') 333 | with open(filepath, mode="w", encoding="utf-8") as f: 334 | json.dump(versionInfo, f, indent=2, ensure_ascii=False) 335 | print_n(f"Save {basename}.civitai.json") 336 | return "Save infos" 337 | 338 | def removeFile(file): 339 | if send2trash_installed: 340 | try: 341 | send2trash(file.replace('/','\\')) 342 | except Exception as e: 343 | print_ly('Error: Fail to move file to trash') 344 | else: 345 | print_lc('Move file to trash') 346 | else: 347 | print_lc('File is not deleted. send2trash module is missing.') 348 | return 349 | 350 | def open_folder(f): 351 | ''' 352 | [reference](https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/5ef669de080814067961f28357256e8fe27544f4/modules/ui_common.py#L109) 353 | ''' 354 | if not f: 355 | return 356 | count = 0 357 | while not os.path.isdir(f): 358 | count += 1 359 | print_lc(f'Not found "{f}"') 360 | newf = os.path.abspath(os.path.join(f, os.pardir)) 361 | if newf == f: 362 | break 363 | if count >5: 364 | print_lc(f'Not found the folder') 365 | return 366 | f = newf 367 | path = os.path.normpath(f) 368 | if os.path.isdir(path): 369 | if platform.system() == "Windows": 370 | # sp.Popen(rf'explorer /select,"{path}"') 371 | # sp.run(["explorer", path]) 372 | os.startfile(path, operation="explore") 373 | elif platform.system() == "Darwin": 374 | # sp.run(["open", path]) 375 | sp.Popen(["open", path]) 376 | elif "microsoft-standard-WSL2" in platform.uname().release: 377 | # sp.run(["wsl-open", path]) 378 | sp.Popen(["explorer.exe", sp.check_output(["wslpath", "-w", path])]) 379 | else: 380 | # sp.run(["xdg-open", path]) 381 | sp.Popen(["xdg-open", path]) 382 | else: 383 | print_lc(f'Not found "{path}"') 384 | 385 | class History(): 386 | def __init__(self, path=None): 387 | self._path = Path.joinpath( 388 | extensionFolder(), Path("../history.json")) 389 | if path != None: 390 | self._path = path 391 | self._history: deque = self.load() 392 | def load(self) -> list[dict]: 393 | try: 394 | with open(self._path, 'r', encoding="utf-8") as f: 395 | ret = json.load(f) 396 | except: 397 | ret = [] 398 | return deque(ret,maxlen=30) 399 | def save(self): 400 | try: 401 | with open(self._path, 'w', encoding="utf-8") as f: 402 | json.dump(list(self._history), f, indent=4, ensure_ascii=False) 403 | except Exception as e: 404 | #print_lc(e) 405 | pass 406 | def len(self) -> int: 407 | return len(self._history) if self._history is not None else None 408 | def getAsChoices(self): 409 | ret = [ 410 | json.dumps(h, ensure_ascii=False) for h in self._history] 411 | return ret 412 | 413 | class KeywordHistory(History): 414 | 415 | def __init__(self, fileName="keyword_history.json"): 416 | super().__init__( 417 | Path.joinpath(extensionFolder(), Path(f"../{fileName}")) 418 | ) 419 | 420 | def add(self, type="Keyword", word=None): 421 | if type == "No" or word == "" or word == None: 422 | return 423 | if type == "User name" and word in FavoriteCreators.getAsList(): 424 | return 425 | d = {"type": type, "word": word} 426 | try: 427 | self._history.remove(d) 428 | except: 429 | pass 430 | self._history.appendleft(d) 431 | while self.len(type) > opts.civsfz_length_of_search_history: 432 | self.pop(type) 433 | self.save() 434 | 435 | def getAsChoices(self, type="Keyword"): 436 | # ret = [f'{w["word"]}{self._delimiter}{w["type"]}' for w in self._history] 437 | ret = [f'{w["word"]}' for w in self._history if w["type"] == type] 438 | # Add favorite users 439 | favUsers = [] 440 | if type == "User name": 441 | favUsers = [f"⭐️{s.strip()}" for s in FavoriteCreators.getAsList()] 442 | return ret + favUsers 443 | 444 | def len(self, type="Keyword"): 445 | return len([ w for w in self._history if w.get("type") == type]) 446 | 447 | def pop(self, type="Keyword"): 448 | j = ( 449 | len(self._history) 450 | - next(i for i, v in enumerate(reversed(self._history)) if v.get("type") == type) 451 | - 1 452 | ) 453 | l = list(self._history) 454 | l.pop(j) 455 | self._history = deque(l) 456 | 457 | 458 | class SearchHistory(History): 459 | def __init__(self): 460 | super().__init__(Path.joinpath( 461 | extensionFolder(), Path("../search_history.json"))) 462 | self._delimiter = "_._" 463 | def add(self, type, word): 464 | if type == "No" or word == "" or word == None: 465 | return 466 | if word in FavoriteCreators.getAsList(): 467 | return 468 | d = { "type" : type, 469 | "word": word } 470 | try: 471 | self._history.remove(d) 472 | except: 473 | pass 474 | self._history.appendleft(d) 475 | while self.len() > opts.civsfz_length_of_search_history: 476 | self._history.pop() 477 | self.save() 478 | def getAsChoices(self): 479 | ret = [f'{w["word"]}{self._delimiter}{w["type"]}' for w in self._history] 480 | # Add favorite users 481 | favUsers = [ 482 | f"{s.strip()}{self._delimiter}User name{self._delimiter}⭐️" 483 | for s in FavoriteCreators.getAsList() 484 | ] 485 | return ret + favUsers 486 | def getDelimiter(self) -> str: 487 | return self._delimiter 488 | 489 | class ConditionsHistory(History): 490 | def __init__(self): 491 | super().__init__(Path.joinpath( 492 | extensionFolder(), Path("../conditions_history.json"))) 493 | self._delimiter = "_._" 494 | def add(self, sort, period, baseModels, nsfw ): 495 | d = { 496 | "sort": sort, 497 | "period": period, 498 | "baseModels": baseModels, 499 | "nsfw": nsfw 500 | } 501 | try: 502 | self._history.remove(d) 503 | except: 504 | pass 505 | self._history.appendleft(d) 506 | while self.len() > opts.civsfz_length_of_conditions_history: 507 | self._history.pop() 508 | self.save() 509 | def getAsChoices(self): 510 | ret = [self._delimiter.join( 511 | [ str(v) if k != 'baseModels' else json.dumps(v, ensure_ascii=False) for k, v in h.items()] 512 | ) for h in self._history] 513 | return ret 514 | def getDelimiter(self) -> str: 515 | return self._delimiter 516 | HistoryKwd = KeywordHistory() 517 | HistoryS = SearchHistory() # deprecated 518 | HistoryC = ConditionsHistory() 519 | 520 | class UserInfo: 521 | def __init__(self, path=None): 522 | self._path = Path.joinpath(extensionFolder(), Path("../users.json")) 523 | if path != None: 524 | self._path = path 525 | self._users: list = self.load() 526 | 527 | def add(self, name: str) -> bool: 528 | name = name.strip() 529 | if name == "": 530 | return False 531 | self.remove(name) 532 | self._users.append(name) 533 | self.save() 534 | return True 535 | 536 | def remove(self, name: str) -> bool: 537 | name = name.strip() 538 | if name == "": 539 | return False 540 | self._users = [u for u in self._users if u != name] 541 | self.save() 542 | return True 543 | 544 | def load(self) -> list: 545 | try: 546 | with open(self._path, "r", encoding="utf-8") as f: 547 | lines = f.readlines() 548 | line = ", ".join(lines) 549 | ret = [s.strip() for s in line.split(",") if s.strip()] 550 | except Exception as e: 551 | # print_lc(f"{e}") 552 | ret = [] 553 | return ret 554 | 555 | def save(self): 556 | try: 557 | with open(self._path, "w", encoding="utf-8") as f: 558 | l = len(self._users) 559 | n = 3 560 | for i in range(0,int(l),n): 561 | f.write(", ".join(self._users[i : i + n])) 562 | f.write("\n") 563 | except Exception as e: 564 | # print_lc(e) 565 | pass 566 | def getAsList(self) -> list[str]: 567 | return self._users 568 | def getAsText(self) -> str: 569 | return ", ".join(self._users) 570 | 571 | class FavoriteUsers(UserInfo): 572 | def __init__(self): 573 | super().__init__( 574 | Path.joinpath(extensionFolder(), Path("../favoriteUsers.txt")) 575 | ) 576 | if hasattr(opts, "civsfz_favorite_creators"): # for backward compatibility 577 | users = [s.strip() for s in opts.civsfz_favorite_creators.split(",") if s.strip()] 578 | for u in users: 579 | self.add(u) 580 | 581 | class BanUsers(UserInfo): 582 | def __init__(self): 583 | super().__init__( 584 | Path.joinpath(extensionFolder(), Path("../bannedUsers.txt"))) 585 | if hasattr(opts, "civsfz_ban_creators"): # for backward compatibility 586 | users = [ 587 | s.strip() for s in opts.civsfz_ban_creators.split(",") if s.strip() 588 | ] 589 | for u in users: 590 | self.add(u) 591 | FavoriteCreators = FavoriteUsers() 592 | BanCreators = BanUsers() 593 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /scripts/civsfz_api.py: -------------------------------------------------------------------------------- 1 | import re 2 | import datetime 3 | from dateutil import tz 4 | import json 5 | import urllib.parse 6 | from pathlib import Path 7 | import requests 8 | # from requests_cache import CachedSession 9 | from colorama import Fore, Back, Style 10 | from scripts.civsfz_filemanage import ( 11 | generate_model_save_path2, 12 | extensionFolder, 13 | FavoriteCreators, 14 | BanCreators, 15 | isExistFile, 16 | ) 17 | from scripts.civsfz_color import dictBasemodelColors 18 | from scripts.civsfz_shared import opts, read_timeout, card_no_preview 19 | from jinja2 import Environment, FileSystemLoader 20 | 21 | print_ly = lambda x: print(Fore.LIGHTYELLOW_EX + "CivBrowser: " + x + Style.RESET_ALL ) 22 | print_lc = lambda x: print(Fore.LIGHTCYAN_EX + "CivBrowser: " + x + Style.RESET_ALL ) 23 | print_n = lambda x: print("CivBrowser: " + x ) 24 | 25 | templatesPath = Path.joinpath( 26 | extensionFolder(), Path("../templates")) 27 | environment = Environment( 28 | loader=FileSystemLoader(templatesPath.resolve()), 29 | extensions=["jinja2.ext.loopcontrols"], 30 | ) 31 | 32 | class Browser: 33 | session = None 34 | 35 | def __init__(self): 36 | if Browser.session is None: 37 | Browser.session = requests.Session() 38 | Browser.session.headers.update( 39 | {'User-Agent': r'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0'}) 40 | #self.setAPIKey("") 41 | def __enter__(self): 42 | return Browser.session 43 | def __exit__(self): 44 | Browser.session.close() 45 | def newSession(self): 46 | Browser.session = requests.Session() 47 | def reConnect(self): 48 | Browser.session.close() 49 | self.newSession() 50 | def setAPIKey(self, api_key): 51 | if len(api_key) == 32: 52 | Browser.session.headers.update( 53 | {"Authorization": f"Bearer {api_key}"}) 54 | print_lc("Apply API key") 55 | 56 | 57 | class ModelCardsPagination: 58 | def __init__(self, response:dict, types:list=None, sort:str=None, searchType:str=None, searchTerm:str=None, nsfw:bool=None, period:str=None, basemodels:list=None) -> None: 59 | self.types = types 60 | self.sort = sort 61 | self.searchType = searchType 62 | self.searchTerm = searchTerm 63 | self.nsfw = nsfw 64 | self.period = period 65 | self.baseModels = basemodels 66 | self.pages=[] 67 | self.pageSize = 1 #response['metadata']['pageSize'] if 'pageSize' in response['metadata'] else None 68 | self.currentPage = 1 69 | page = { 'url': response['requestUrl'], 70 | 'nextUrl': response['metadata']['nextPage'] if 'nextPage' in response['metadata'] else None, 71 | 'prevUrl': None 72 | } 73 | self.pages.append(page) 74 | 75 | def getNextUrl(self) -> str: 76 | return self.pages[self.currentPage-1]['nextUrl'] 77 | def getPrevUrl(self) -> str: 78 | return self.pages[self.currentPage-1]['prevUrl'] 79 | def getJumpUrl(self, page) -> str: 80 | if page <= len(self.pages): 81 | return self.pages[page-1]['url'] 82 | else: 83 | return None 84 | def getPagination(self): 85 | ret = { "types": self.types, 86 | "sort": self.sort, 87 | "searchType": self.searchType, 88 | "searchTerm": self.searchTerm, 89 | "nsfw": self.nsfw, 90 | "period": self.period, 91 | "basemodels": self.baseModels, 92 | "pageSize": len(self.pages), 93 | "currentPage": self.currentPage, 94 | "pages": self.pages 95 | } 96 | return ret 97 | def setPagination(self, pagination:dict): 98 | self.pages = pagination['pages'] 99 | self.currentPage = pagination['currentPage'] 100 | self.pageSize = pagination['pageSize'] 101 | 102 | def nextPage(self, response:dict) -> None: 103 | prevUrl = self.pages[self.currentPage-1]['url'] 104 | page = { 'url': response['requestUrl'], 105 | 'nextUrl': response['metadata']['nextPage'] if 'nextPage' in response['metadata'] else None, 106 | 'prevUrl': prevUrl 107 | } 108 | #if self.currentPage + 1 == self.pageSize: 109 | # page['nextUrl'] = None 110 | if self.currentPage < len(self.pages): 111 | self.pages[self.currentPage] = page 112 | else: 113 | self.pages.append(page) 114 | if len(self.pages) > self.pageSize: 115 | self.pageSize = len(self.pages) 116 | self.currentPage += 1 117 | return 118 | 119 | def prevPage(self, response: dict) -> None: 120 | page = {'url': response['requestUrl'], 121 | 'nextUrl': response['metadata']['nextPage'] if 'nextPage' in response['metadata'] else None, 122 | 'prevUrl': None 123 | } 124 | if self.currentPage > 1: 125 | page['prevUrl'] = self.pages[self.currentPage-2]['url'] 126 | self.pages[self.currentPage-1] = page 127 | self.currentPage -= 1 128 | return 129 | def pageJump(self, response, pageNum): 130 | page = {'url': response['requestUrl'], 131 | 'nextUrl': response['metadata']['nextPage'] if 'nextPage' in response['metadata'] else None, 132 | 'prevUrl': None 133 | } 134 | if pageNum > 1: 135 | page['prevUrl'] = self.pages[pageNum-1]['prevUrl'] 136 | self.pages[pageNum-1] = page 137 | self.currentPage = pageNum 138 | return 139 | 140 | class APIInformation(): 141 | baseUrl = "https://civitai.com" 142 | modelsApi = f"{baseUrl}/api/v1/models" 143 | imagesApi = f"{baseUrl}/api/v1/images" 144 | versionsAPI = f"{baseUrl}/api/v1/model-versions" 145 | byHashAPI = f"{baseUrl}/api/v1/model-versions/by-hash" 146 | typeOptions:list = None 147 | sortOptions:list = None 148 | basemodelOptions:list = None 149 | periodOptions:list = None 150 | searchTypes = [ 151 | "No", 152 | "Keyword", 153 | "User name", 154 | "Tag", 155 | "Model ID", 156 | "Version ID", 157 | "Hash", 158 | ] 159 | nsfwLevel = {"PG": 1, 160 | "PG-13": 2, 161 | "R": 4, 162 | "X": 8, 163 | "XXX": 16, 164 | #"Blocked": 32, 165 | "Banned": 256, 166 | } 167 | def __init__(self) -> None: 168 | if APIInformation.typeOptions is None: 169 | self.getOptions() 170 | def setBaseUrl(self,url:str): 171 | APIInformation.baseUrl = url 172 | def getBaseUrl(self) -> str: 173 | return APIInformation.baseUrl 174 | def getModelsApiUrl(self, id=None): 175 | url = APIInformation.modelsApi 176 | url += f'/{id}' if id is not None else "" 177 | return url 178 | def getImagesApiUrl(self): 179 | return APIInformation.imagesApi 180 | def getVersionsApiUrl(self, id=None): 181 | url = APIInformation.versionsAPI 182 | url += f'/{id}' if id is not None else "" 183 | return url 184 | def getVersionsByHashUrl(self, hash=None): 185 | url = APIInformation.byHashAPI 186 | url += f'/{hash}' if id is not None else "" 187 | return url 188 | def getTypeOptions(self) -> list: 189 | # global typeOptions, sortOptions, basemodelOptions 190 | return APIInformation.typeOptions 191 | def getSortOptions(self) -> list: 192 | # global typeOptions, sortOptions, basemodelOptions 193 | return APIInformation.sortOptions 194 | def getBasemodelOptions(self) -> list: 195 | # global typeOptions, sortOptions, basemodelOptions 196 | return APIInformation.basemodelOptions 197 | def getPeriodOptions(self) -> list: 198 | return APIInformation.periodOptions 199 | def getSearchTypes(self) -> list: 200 | return APIInformation.searchTypes 201 | def strNsfwLevel(self, nsfwLevel:int) -> str: 202 | keys = [] 203 | for k, v in APIInformation.nsfwLevel.items(): 204 | if nsfwLevel & v > 0: 205 | keys.append(k) 206 | return ", ".join(keys) 207 | 208 | def requestApiOptions(self, url=None, query=None): 209 | if url is None: 210 | url = self.getModelsApiUrl() 211 | if query is not None: 212 | query = urllib.parse.urlencode( 213 | query, doseq=True, quote_via=urllib.parse.quote) 214 | # print_lc(f'{query=}') 215 | 216 | # Make a GET request to the API 217 | try: 218 | # with requests.Session() as request: 219 | browser = Browser() 220 | response = browser.session.get( 221 | url, params=query, timeout=read_timeout() 222 | ) 223 | # print_lc(f'Page cache: {response.headers["CF-Cache-Status"]}') 224 | response.raise_for_status() 225 | except requests.exceptions.RequestException as e: 226 | # print(f"{(response.status_code)=}") 227 | if response.status_code == 400: # Bad Request 228 | # expected under normal conditions 229 | response.encoding = "utf-8" 230 | data = ( 231 | json.loads(response.text) 232 | ) 233 | else: 234 | data = "" 235 | print_ly("Civitai server may be down or under maintenance.") 236 | else: 237 | data = "" 238 | # Check the status code of the response 239 | # if response.status_code != 200: 240 | # print("Request failed with status code: {}".format(response.status_code)) 241 | # exit() 242 | return data 243 | 244 | def getOptions(self): 245 | '''Get choices from Civitai''' 246 | url = self.getModelsApiUrl() 247 | query = { 'types': ""} 248 | data = self.requestApiOptions(url, query) 249 | types = [ 250 | "Checkpoint", 251 | "TextualInversion", 252 | "Hypernetwork", 253 | "AestheticGradient", 254 | "LORA", 255 | "LoCon", 256 | "DoRA", 257 | "Controlnet", 258 | "Upscaler", 259 | "MotionModule", 260 | "VAE", 261 | "Poses", 262 | "Wildcards", 263 | "Workflows", 264 | "Detection", 265 | "Other" 266 | ] 267 | try: 268 | # types = data['error']['issues'][0]['unionErrors'][0]['issues'][0]['options'] 269 | res = json.loads(data['error']['message']) 270 | newList = res[0]["errors"][0][0]["values"] 271 | # print_lc(f"{res[0]['errors'][0][0]['values']=}") 272 | diff = set(types) ^ set(newList) 273 | if len(diff) != 0: 274 | print_lc(f"Type options have been updated.\n{diff=}") 275 | types = newList 276 | except: 277 | print_ly(f'ERROR: Get types') 278 | else: 279 | # print_lc(f'Set types') 280 | pass 281 | priorityTypes = [ 282 | "Checkpoint", 283 | "TextualInversion", 284 | "LORA", 285 | "LoCon", 286 | "DoRA" 287 | ] 288 | dictPriority = { 289 | priorityTypes[i]: priorityTypes[i] for i in range(0, len(priorityTypes))} 290 | dict_types = dictPriority | {types[i]: types[i] 291 | for i in range(0, len(types))} 292 | APIInformation.typeOptions = [dictPriority.get( 293 | key, key) for key, value in dict_types.items()] 294 | 295 | query = {'baseModels': ""} 296 | data = self.requestApiOptions(url, query) 297 | APIInformation.basemodelOptions = [ 298 | "AuraFlow", 299 | "Chroma", 300 | "CogVideoX", 301 | "Flux.1 S", 302 | "Flux.1 D", 303 | "Flux.1 Krea", 304 | "Flux.1 Kontext", 305 | "Flux.2 D", 306 | "HiDream", 307 | "Hunyuan 1", 308 | "Hunyuan Video", 309 | "Illustrious", 310 | "Imagen4", 311 | "Kolors", 312 | "LTXV", 313 | "Lumina", 314 | "Mochi", 315 | "Nano Banana", 316 | "NoobAI", 317 | "ODOR", 318 | "OpenAI", 319 | "Other", 320 | "PixArt a", 321 | "PixArt E", 322 | "Playground v2", 323 | "Pony", 324 | "Pony V7", 325 | "Qwen", 326 | "Stable Cascade", 327 | "SD 1.4", 328 | "SD 1.5", 329 | "SD 1.5 LCM", 330 | "SD 1.5 Hyper", 331 | "SD 2.0", 332 | "SD 2.0 768", 333 | "SD 2.1", 334 | "SD 2.1 768", 335 | "SD 2.1 Unclip", 336 | "SD 3", 337 | "SD 3.5", 338 | "SD 3.5 Large", 339 | "SD 3.5 Large Turbo", 340 | "SD 3.5 Medium", 341 | "Sora 2", 342 | "SDXL 0.9", 343 | "SDXL 1.0", 344 | "SDXL 1.0 LCM", 345 | "SDXL Lightning", 346 | "SDXL Hyper", 347 | "SDXL Turbo", 348 | "SDXL Distilled", 349 | "Seedream", 350 | "SVD", 351 | "SVD XT", 352 | "Veo 3", 353 | "Wan Video", 354 | "Wan Video 1.3B t2v", 355 | "Wan Video 14B t2v", 356 | "Wan Video 14B i2v 480p", 357 | "Wan Video 14B i2v 720p", 358 | "Wan Video 2.2 TI2V-5B", 359 | "Wan Video 2.2 I2V-A14B", 360 | "Wan Video 2.2 T2V-A14B", 361 | "Wan Video 2.5 T2V", 362 | "Wan Video 2.5 I2V", 363 | "ZImageTurbo", 364 | ] 365 | try: 366 | # APIInformation.basemodelOptions = data['error']['issues'][0]['unionErrors'][0]['issues'][0]['options'] 367 | res = json.loads(data['error']['message']) 368 | # print_lc(f"{res[0]['errors'][0][0]['values']=}") 369 | newList = res[0]["errors"][0][0]["values"] 370 | diff = set(APIInformation.basemodelOptions) ^ set(newList) 371 | if len(diff) != 0: 372 | print_lc(f"Base model options have been updated.\n{diff=}") 373 | APIInformation.basemodelOptions = newList 374 | except: 375 | print_ly(f'ERROR: Get base models') 376 | else: 377 | # print_lc(f'Set base models') 378 | pass 379 | 380 | query = {'sort': ""} 381 | data = self.requestApiOptions(url, query) 382 | APIInformation.sortOptions = [ 383 | "Highest Rated", 384 | "Most Downloaded", 385 | "Most Liked", 386 | "Most Discussed", 387 | "Most Collected", 388 | "Most Images", 389 | "Newest", 390 | "Oldest", 391 | ] 392 | try: 393 | # APIInformation.sortOptions = data['error']['issues'][0]['options'] 394 | res = json.loads(data['error']['message']) 395 | # print_lc(f"{res[0]['values']=}") 396 | newList = res[0]["values"] 397 | diff = set(APIInformation.sortOptions) ^ set(newList) 398 | if len(diff) != 0: 399 | print_lc(f"Sort options have been updated.\n{diff=}") 400 | APIInformation.sortOptions = newList 401 | except: 402 | print_ly(f'ERROR: Get sorts') 403 | else: 404 | # print_lc(f'Set sorts') 405 | pass 406 | 407 | query = {'period': ""} 408 | data = self.requestApiOptions(url, query) 409 | APIInformation.periodOptions = [ 410 | "Day", 411 | "Week", 412 | "Month", 413 | "Year", 414 | "AllTime" 415 | ] 416 | try: 417 | # APIInformation.periodOptions = data['error']['issues'][0]['options'] 418 | res = json.loads(data['error']['message']) 419 | # print_lc(f"{res[0]['values']=}") 420 | newList = res[0]["values"] 421 | diff = set(APIInformation.periodOptions) ^ set(newList) 422 | if len(diff) != 0: 423 | print_lc(f"Period options have been updated.\n{diff=}") 424 | APIInformation.periodOptions = newList 425 | except: 426 | print_ly(f'ERROR: Get periods') 427 | else: 428 | # print_lc(f'Set periods') 429 | pass 430 | 431 | class CivitaiModels(APIInformation): 432 | '''CivitaiModels: Handle the response of civitai models api v1.''' 433 | def __init__(self, url:str=None, json_data:dict=None, content_type:str=None): 434 | super().__init__() 435 | self.jsonData = json_data 436 | # self.contentType = content_type 437 | self.showNsfw = False 438 | self.baseUrl = APIInformation.baseUrl if url is None else url 439 | self.modelIndex = None 440 | self.versionsInfo = None # for radio button and file exist check 441 | self.versionIndex = None 442 | self.modelVersionInfo = None 443 | self.requestError = None 444 | self.saveFolder = None 445 | self.cardPagination = None 446 | def updateJsonData(self, json_data:dict=None, content_type:str=None): 447 | '''Update json data.''' 448 | self.jsonData = json_data 449 | # self.contentType = self.contentType if content_type is None else content_type 450 | self.showNsfw = False 451 | self.modelIndex = None 452 | self.versionIndex = None 453 | self.modelVersionInfo = None 454 | self.requestError = None 455 | self.saveFolder = None 456 | def getJsonData(self) -> dict: 457 | return self.jsonData 458 | 459 | def setShowNsfw(self, showNsfw:bool): 460 | self.showNsfw = showNsfw 461 | def isShowNsfw(self) -> bool: 462 | return self.showNsfw 463 | # def setContentType(self, content_type:str): 464 | # self.contentType = content_type 465 | # def getContentType(self) -> str: 466 | # return self.contentType 467 | def getRequestError(self) -> requests.exceptions.RequestException: 468 | return self.requestError 469 | def clearRequestError(self): 470 | self.requestError = None 471 | def setSaveFolder(self, path): 472 | self.saveFolder = path 473 | def getSaveFolder(self): 474 | return self.saveFolder 475 | def matchLevel(self, modelLevel:int, browsingLevel:int) -> bool: 476 | if bool(browsingLevel & self.nsfwLevel["Banned"]) : 477 | # Show banned users 478 | if browsingLevel == self.nsfwLevel["Banned"]: 479 | # Show all if Browsing Level is unchecked 480 | return True 481 | return modelLevel & browsingLevel > 0 482 | else: 483 | if browsingLevel == 0: # Show all if Browsing Level is unchecked 484 | return not bool(modelLevel & self.nsfwLevel["Banned"]) 485 | return bool(modelLevel & browsingLevel) and not bool( 486 | modelLevel & self.nsfwLevel["Banned"] 487 | ) 488 | 489 | # Models 490 | def getModels(self, showNsfw = False) -> list: 491 | '''Return: [(str: Model name, str: index)]''' 492 | model_list = [] 493 | for index, item in enumerate(self.jsonData['items']): 494 | # print_lc( 495 | # f"{item['nsfwLevel']}-{item['nsfwLevel'] & sum(opts.civsfz_browsing_level)}-{opts.civsfz_browsing_level}") 496 | # if (item['nsfwLevel'] & 2*sum(opts.civsfz_browsing_level)-1) > 0: 497 | if showNsfw: 498 | model_list.append((item['name'], index)) 499 | elif not self.treatAsNsfw(modelIndex=index): #item['nsfw']: 500 | model_list.append((item['name'], index)) 501 | return model_list 502 | 503 | # def getModelNames(self) -> dict: #include nsfw models 504 | # model_dict = {} 505 | # for item in self.jsonData['items']: 506 | # model_dict[item['name']] = item['name'] 507 | # return model_dict 508 | # def getModelNamesSfw(self) -> dict: #sfw models 509 | # '''Return SFW items names.''' 510 | # model_dict = {} 511 | # for item in self.jsonData['items']: 512 | # if not item['nsfw']: 513 | # model_dict[item['name']] = item['name'] 514 | # return model_dict 515 | 516 | # Model 517 | def getModelNameByID(self, id:int) -> str: 518 | name = None 519 | for item in self.jsonData['items']: 520 | if int(item['id']) == int(id): 521 | name = item['name'] 522 | return name 523 | def getIDByModelName(self, name:str) -> str: 524 | id = None 525 | for item in self.jsonData['items']: 526 | if item['name'] == name: 527 | id = int(item['id']) 528 | return id 529 | def getModelNameByIndex(self, index:int) -> str: 530 | return self.jsonData['items'][index]['name'] 531 | def isNsfwModelByID(self, id:int) -> bool: 532 | nsfw = None 533 | for item in self.jsonData['items']: 534 | if int(item['id']) == int(id): 535 | nsfw = item['nsfw'] 536 | return nsfw 537 | def selectModelByIndex(self, index:int): 538 | if index >= 0 and index < len(self.jsonData['items']): 539 | self.modelIndex = index 540 | return self.modelIndex 541 | def selectModelByID(self, id:int): 542 | for index, item in enumerate(self.jsonData['items']): 543 | if int(item['id']) == int(id): 544 | self.modelIndex = index 545 | return self.modelIndex 546 | def selectModelByName(self, name:str) -> int: 547 | if name is not None: 548 | for index, item in enumerate(self.jsonData['items']): 549 | if item['name'] == name: 550 | self.modelIndex = index 551 | # print(f'{name} - {self.modelIndex}') 552 | return self.modelIndex 553 | def isNsfwModel(self) -> bool: 554 | return self.jsonData['items'][self.modelIndex]['nsfw'] 555 | def treatAsNsfw(self, modelIndex=None, versionIndex=None): 556 | modelIndex = self.modelIndex if modelIndex is None else modelIndex 557 | modelIndex = 0 if modelIndex is None else modelIndex 558 | versionIndex = self.versionIndex if versionIndex is None else versionIndex 559 | versionIndex = 0 if versionIndex is None else versionIndex 560 | ret = self.jsonData['items'][modelIndex]['nsfw'] 561 | if opts.civsfz_treat_x_as_nsfw: 562 | try: 563 | picNsfw = self.jsonData['items'][modelIndex]['modelVersions'][versionIndex]['images'][0]['nsfwLevel'] 564 | except Exception as e: 565 | # print_ly(f'{e}') 566 | pass 567 | else: 568 | # print_lc(f'{picNsfw}') 569 | if picNsfw > 1: 570 | ret = True 571 | return ret 572 | def getIndexByModelName(self, name:str) -> int: 573 | retIndex = None 574 | if name is not None: 575 | for index, item in enumerate(self.jsonData['items']): 576 | if item['name'] == name: 577 | retIndex = index 578 | return retIndex 579 | def getSelectedModelIndex(self) -> int: 580 | return self.modelIndex 581 | def getSelectedModelName(self) -> str: 582 | item = self.jsonData['items'][self.modelIndex] 583 | return item['name'] 584 | def getSelectedModelID(self) -> str: 585 | item = self.jsonData['items'][self.modelIndex] 586 | return int(item['id']) 587 | def getSelectedModelType(self) -> str: 588 | item = self.jsonData['items'][self.modelIndex] 589 | return item['type'] 590 | def getModelTypeByIndex(self, index:int) -> str: 591 | item = self.jsonData['items'][index] 592 | return item['type'] 593 | def getUserName(self): 594 | item = self.jsonData['items'][self.modelIndex] 595 | return item['creator']['username'] if 'creator' in item else "" 596 | def getModelID(self): 597 | item = self.jsonData['items'][self.modelIndex] 598 | return item['id'] 599 | 600 | def allows2permissions(self) -> dict: 601 | '''Convert allows to permissions. Select model first. 602 | [->Reference](https://github.com/civitai/civitai/blob/main/src/components/PermissionIndicator/PermissionIndicator.tsx#L15)''' 603 | permissions = {} 604 | if self.modelIndex is None: 605 | print_ly('Select item first.') 606 | else: 607 | if self.modelIndex is not None: 608 | canSellImagesPermissions = { 609 | 'Image'} 610 | canRentCivitPermissions = {'RentCivit'} 611 | canRentPermissions = {'Rent'} 612 | canSellPermissions = {'Sell'} 613 | 614 | item = self.jsonData['items'][self.modelIndex] 615 | allowCommercialUse = set(item['allowCommercialUse']) 616 | allowNoCredit = item['allowNoCredit'] 617 | allowDerivatives = item['allowDerivatives'] 618 | allowDifferentLicense = item['allowDifferentLicense'] 619 | 620 | canSellImages = len(allowCommercialUse & canSellImagesPermissions) > 0 621 | canRentCivit = len(allowCommercialUse & canRentCivitPermissions) > 0 622 | canRent = len(allowCommercialUse & canRentPermissions) > 0 623 | canSell = len(allowCommercialUse & canSellPermissions) > 0 624 | 625 | permissions['allowNoCredit'] = allowNoCredit 626 | permissions['canSellImages'] = canSellImages 627 | permissions['canRentCivit'] = canRentCivit 628 | permissions['canRent'] = canRent 629 | permissions['canSell'] = canSell 630 | permissions['allowDerivatives'] = allowDerivatives 631 | permissions['allowDifferentLicense'] = allowDifferentLicense 632 | return permissions 633 | def getModelVersionsList(self) -> list: 634 | '''Return modelVersions list. Select item before.''' 635 | self.getModelVersionsInfo() 636 | return [(item["name"], i) for i, item in enumerate(self.versionsInfo)] 637 | # versionNames = [] 638 | # if self.modelIndex is None: 639 | # print_ly('Select item first.') 640 | # else: 641 | # item = self.jsonData['items'][self.modelIndex] 642 | # versionNames = [ (version['name'],i) for i,version in enumerate(item['modelVersions'])] 643 | # # versionNames[version['name']] = version["name"] 644 | # return versionNames 645 | 646 | def modelVersionsInfo(self) -> list: 647 | return self.versionsInfo 648 | 649 | def getModelVersionsInfo(self) -> list: 650 | info = [] 651 | if self.modelIndex is None: 652 | pass 653 | # print_ly("Select item first.") 654 | else: 655 | hasVersions = self.checkAlreadyHave() 656 | # print_lc(f"{hasVersions=}") 657 | item = self.jsonData["items"][self.modelIndex] 658 | info = [ 659 | { 660 | "name": l1["name"], 661 | "base_model": l1["baseModel"], 662 | "have": l2, 663 | } 664 | for l1,l2 in zip(item["modelVersions"], hasVersions) 665 | ] 666 | self.versionsInfo = info 667 | return info 668 | 669 | def checkAlreadyHave(self, index:int=None) -> list[bool]: 670 | if index == None: 671 | index = self.modelIndex 672 | item = self.jsonData["items"][index] 673 | hasVersions = [] 674 | for i,ver in enumerate(item['modelVersions']): 675 | have = False 676 | for file in ver['files']: 677 | folder = generate_model_save_path2( 678 | item["type"], 679 | item["name"], 680 | ver["baseModel"], 681 | self.treatAsNsfw(modelIndex=index, versionIndex=i), 682 | item["creator"]["username"] if "creator" in item else "", 683 | item["id"], 684 | ver["id"], 685 | ver["name"], 686 | ) 687 | # print(f"{folder}") 688 | file_name = file['name'] 689 | path_file = folder / Path(file_name) 690 | # print(f"{path_file}") 691 | if isExistFile(folder, file_name): 692 | have = True 693 | break 694 | hasVersions.append(have) 695 | return hasVersions 696 | 697 | # Version 698 | def selectVersionByIndex(self, index:int) -> int: 699 | numVersions = len(self.jsonData['items'][self.modelIndex]['modelVersions']) 700 | index = 0 if index < 0 else index 701 | index = numVersions -1 if index > numVersions-1 else index 702 | self.versionIndex = index 703 | return self.versionIndex 704 | def selectVersionByID(self, ID:int) -> int: 705 | item = self.jsonData['items'][self.modelIndex] 706 | for index, model in enumerate(item['modelVersions']): 707 | if int(model['id']) == int(ID): 708 | self.versionIndex = index 709 | return self.versionIndex 710 | def selectVersionByName(self, name:str) -> int: 711 | '''Select model version by name. Select model first. 712 | 713 | Args: 714 | ID (int): version ID 715 | Returns: 716 | int: index number of the version 717 | ''' 718 | if name is not None: 719 | item = self.jsonData['items'][self.modelIndex] 720 | for index, model in enumerate(item['modelVersions']): 721 | if model['name'] == name: 722 | self.versionIndex = index 723 | return self.versionIndex 724 | def getSelectedVersionName(self): 725 | return self.jsonData['items'][self.modelIndex]['modelVersions'][self.versionIndex]['name'] 726 | def getSelectedVersionBaseModel(self): 727 | # print(f"{self.jsonData['items'][self.modelIndex]['modelVersions']}") 728 | return self.jsonData['items'][self.modelIndex]['modelVersions'][self.versionIndex]['baseModel'] 729 | def getSelectedVersionEarlyAccessTimeFrame(self): 730 | ''' 731 | earlyAccessTimeFrame is missing from the API response 732 | ''' 733 | return self.jsonData['items'][self.modelIndex]['modelVersions'][self.versionIndex]['earlyAccessTimeFrame'] 734 | def getSelectedVersionEarlyAccessDeadline(self): 735 | # if 'earlyAccessDeadline' in self.jsonData['items'][self.modelIndex]['modelVersions'][self.versionIndex]: 736 | # return self.jsonData['items'][self.modelIndex]['modelVersions'][self.versionIndex]['earlyAccessDeadline'] 737 | if self.jsonData['items'][self.modelIndex]['modelVersions'][self.versionIndex]['availability'] == "EarlyAccess": 738 | return "EA" 739 | else: 740 | return "" 741 | def setModelVersionInfo(self, modelInfo: str): 742 | self.modelVersionInfo = modelInfo 743 | def getModelVersionInfo(self) -> str: 744 | return self.modelVersionInfo 745 | def getVersionDict(self) -> dict: 746 | version_dict = {} 747 | item = self.jsonData['items'][self.modelIndex] 748 | version_dict = item['modelVersions'][self.versionIndex] 749 | return version_dict 750 | 751 | def getCreatedDatetime(self) -> datetime.datetime : 752 | item = self.jsonData['items'][self.modelIndex] 753 | version_dict = item['modelVersions'][self.versionIndex] 754 | strCreatedAt = version_dict['createdAt'].replace('Z', '+00:00') # < Python 3.11 755 | dtCreatedAt = datetime.datetime.fromisoformat(strCreatedAt) 756 | # print_lc(f'{dtCreatedAt} {dtCreatedAt.tzinfo}') 757 | return dtCreatedAt 758 | def getUpdatedDatetime(self) -> datetime.datetime: 759 | item = self.jsonData['items'][self.modelIndex] 760 | version_dict = item['modelVersions'][self.versionIndex] 761 | strUpdatedAt = version_dict['updatedAt'].replace( 762 | 'Z', '+00:00') # < Python 3.11 763 | dtUpdatedAt = datetime.datetime.fromisoformat(strUpdatedAt) 764 | # print_lc(f'{dtUpdatedAt} {dtUpdatedAt.tzinfo}') 765 | return dtUpdatedAt 766 | def getPublishedDatetime(self) -> datetime.datetime: 767 | item = self.jsonData['items'][self.modelIndex] 768 | version_dict = item['modelVersions'][self.versionIndex] 769 | if version_dict['publishedAt'] is None: 770 | return None 771 | if version_dict['publishedAt'][-1] == "Z": 772 | strPublishedAt = version_dict['publishedAt'].replace( 773 | 'Z', '+00:00') # < Python 3.11 774 | else: 775 | strPublishedAt = version_dict['publishedAt'][:19] + '+00:00' 776 | # print_lc(f'{version_dict["publishedAt"]=} {strPublishedAt=}') 777 | dtPublishedAt = datetime.datetime.fromisoformat(strPublishedAt) 778 | # print_lc(f'{dtPublishedAt} {dtPublishedAt.tzinfo}') 779 | return dtPublishedAt 780 | def getEarlyAccessDeadlineDatetime(self) -> datetime.datetime: 781 | item = self.jsonData['items'][self.modelIndex] 782 | version_dict = item['modelVersions'][self.versionIndex] 783 | if 'earlyAccessDeadline' in version_dict: 784 | strEarlyAccessDeadline = version_dict['earlyAccessDeadline'].replace( 785 | 'Z', '+00:00') # < Python 3.11 786 | dtEarlyAccessDeadline = datetime.datetime.fromisoformat(strEarlyAccessDeadline) 787 | else: 788 | dtEarlyAccessDeadline="" 789 | # print_lc(f'{dtPublishedAt} {dtPublishedAt.tzinfo}') 790 | return dtEarlyAccessDeadline 791 | def getVersionID(self): 792 | item = self.jsonData['items'][self.modelIndex] 793 | version_dict = item['modelVersions'][self.versionIndex] 794 | return version_dict['id'] 795 | 796 | def addMetaVID(self, vID, modelInfo: dict) -> dict: 797 | versionRes = self.requestVersionByVersionID(vID) 798 | if versionRes is not None: 799 | # print_lc(f'{len(modelInfo["modelVersions"][0]["images"])}-{len(versionRes["images"])}') 800 | for i, img in enumerate(versionRes["images"]): 801 | # if 'meta' not in modelInfo["modelVersions"][0]["images"][i]: 802 | modelInfo["modelVersions"][0]["images"][i]["meta"] = img['meta'] if 'meta' in img else { 803 | } 804 | for i, img in enumerate(modelInfo["modelVersions"][0]["images"]): 805 | if 'meta' not in img: 806 | img["meta"] = { 807 | "WARNING": "Model Version API has no information"} 808 | else: 809 | for i, img in enumerate(modelInfo["modelVersions"][0]["images"]): 810 | if 'meta' not in img: 811 | img["meta"] = { 812 | "WARNING": "Model Version API request error"} 813 | return modelInfo 814 | 815 | def addMetaIID(self, vID:dict, modelInfo:dict) -> dict: 816 | imagesRes = self.requestImagesByVersionId(vID) 817 | if self.requestError is not None: 818 | print_ly(f"Version ID API Request Error: Fail to get meta info.") 819 | if imagesRes is not None: 820 | IDs = {item["id"]: item["meta"] for item in imagesRes["items"] if 'meta' in item} 821 | for i, img in enumerate(modelInfo["modelVersions"][0]["images"]): 822 | if 'id' not in img: 823 | # Extract image ID from url 824 | id = re.findall(r'/(\d+)\.\w+$', img['url']) 825 | # print_lc(f'{img["url"]} {id=}') 826 | if 'id' not in img: 827 | img['id'] = int(id[0]) 828 | 829 | if 'id' in img: 830 | if img['id'] in IDs.keys(): 831 | img['meta']=IDs[img['id']] 832 | else: 833 | img['meta'] = { 834 | "WARNING": "Images API has no information"} 835 | else: 836 | img['meta'] = { 837 | "WARNING": "Version ID API response does not include Image ID. Therefore the Infotext cannot be determined. Try searching by model name."} 838 | else: 839 | for i, img in enumerate(modelInfo["modelVersions"][0]["images"]): 840 | if 'meta' not in img: 841 | img["meta"] = {"ERROR": "Images API request error"} 842 | return modelInfo 843 | 844 | def makeModelInfo2(self, modelIndex=None, versionIndex=None, nsfwLevel=0) -> dict: 845 | """make selected version info""" 846 | modelIndex = self.modelIndex if modelIndex is None else modelIndex 847 | versionIndex = self.versionIndex if versionIndex is None else versionIndex 848 | item = self.jsonData["items"][modelIndex] 849 | version = item["modelVersions"][versionIndex] 850 | modelInfo = {"infoVersion": "2.4"} 851 | for key, value in item.items(): 852 | if key not in ("modelVersions"): 853 | modelInfo[key] = value 854 | modelInfo["allow"] = {} 855 | modelInfo["allow"]["allowNoCredit"] = item["allowNoCredit"] 856 | modelInfo["allow"]["allowCommercialUse"] = item["allowCommercialUse"] 857 | modelInfo["allow"]["allowDerivatives"] = item["allowDerivatives"] 858 | modelInfo["allow"]["allowDifferentLicense"] = item["allowDifferentLicense"] 859 | modelInfo["modelVersions"] = [version] 860 | modelInfo["modelVersions"][0]["files"] = version["files"] 861 | modelInfo["modelVersions"][0]["images"] = version["images"] 862 | # add version info 863 | modelInfo['versionId'] = version['id'] 864 | modelInfo['versionName'] = version['name'] 865 | # modelInfo['createdAt'] = version['createdAt'] 866 | # modelInfo['updatedAt'] = version['updatedAt'] 867 | modelInfo['publishedAt'] = version['publishedAt'] 868 | modelInfo['trainedWords'] = version['trainedWords'] if 'trainedWords' in version else "" 869 | modelInfo['baseModel'] = version['baseModel'] 870 | modelInfo['versionDescription'] = version['description'] if 'description' in version else None 871 | modelInfo["downloadUrl"] = ( 872 | version["downloadUrl"] if "downloadUrl" in version else None 873 | ) 874 | # self.addMetaVID(version["id"], modelInfo) 875 | self.addMetaIID(version["id"], modelInfo) 876 | html = self.modelInfoHtml(modelInfo, nsfwLevel) 877 | modelInfo["html"] = html 878 | modelInfo["html0"] = self.modelInfoHtml(modelInfo, 0) 879 | self.setModelVersionInfo(modelInfo) 880 | return modelInfo 881 | 882 | def getUrlByName(self, model_filename=None): 883 | if self.modelIndex is None: 884 | # print(Fore.LIGHTYELLOW_EX + f'getUrlByName: Select model first. {model_filename}' + Style.RESET_ALL ) 885 | return 886 | if self.versionIndex is None: 887 | # print(Fore.LIGHTYELLOW_EX + f'getUrlByName: Select version first. {model_filename}' + Style.RESET_ALL ) 888 | return 889 | # print(Fore.LIGHTYELLOW_EX + f'File name . {model_filename}' + Style.RESET_ALL ) 890 | item = self.jsonData['items'][self.modelIndex] 891 | version = item['modelVersions'][self.versionIndex] 892 | dl_url = None 893 | for file in version['files']: 894 | if file['name'] == model_filename: 895 | dl_url = file['downloadUrl'] 896 | return dl_url 897 | def getHashByName(self, model_filename=None): 898 | if self.modelIndex is None: 899 | # print(Fore.LIGHTYELLOW_EX + f'getUrlByName: Select model first. {model_filename}' + Style.RESET_ALL ) 900 | return 901 | if self.versionIndex is None: 902 | # print(Fore.LIGHTYELLOW_EX + f'getUrlByName: Select version first. {model_filename}' + Style.RESET_ALL ) 903 | return 904 | # print(Fore.LIGHTYELLOW_EX + f'File name . {model_filename}' + Style.RESET_ALL ) 905 | item = self.jsonData['items'][self.modelIndex] 906 | version = item['modelVersions'][self.versionIndex] 907 | sha256 = "" 908 | for file in version['files']: 909 | # print_lc(f'{file["hashes"]=}') 910 | if file['name'] == model_filename and 'SHA256' in file['hashes']: 911 | sha256 = file['hashes']['SHA256'] 912 | return sha256 913 | 914 | # Pages 915 | def addFirstPage(self, response:dict, types:list=None, sort:str=None, searchType:str=None, 916 | searchTerm:str=None, nsfw:bool=None, period:str=None, basemodels:list=None) -> None: 917 | self.cardPagination = ModelCardsPagination(response, types, sort, searchType, searchTerm, nsfw, period, basemodels) 918 | # print_lc(f'{self.cardPagination.getPagination()=}') 919 | def addNextPage(self, response:dict) -> None: 920 | self.cardPagination.nextPage(response) 921 | def backPage(self, response:dict) -> None: 922 | self.cardPagination.prevPage(response) 923 | def getJumpUrl(self, page) -> str: 924 | return self.cardPagination.getJumpUrl(page) 925 | def pageJump(self, response:dict, page) -> None: 926 | self.cardPagination.pageJump(response, page) 927 | def getPagination(self): 928 | return self.cardPagination.getPagination() 929 | 930 | def getCurrentPage(self) -> str: 931 | # return f"{self.jsonData['metadata']['currentPage']}" 932 | return self.cardPagination.currentPage if self.cardPagination is not None else 0 933 | def getTotalPages(self) -> str: 934 | # return f"{self.jsonData['metadata']['totalPages']}" 935 | # return f"{self.jsonData['metadata']['pageSize']}" 936 | return self.cardPagination.pageSize 937 | def getPages(self) -> str: 938 | return f"{self.getCurrentPage()}/{self.getTotalPages()}" 939 | def nextPage(self) -> str: 940 | # return self.jsonData['metadata']['nextPage'] if 'nextPage' in self.jsonData['metadata'] else None 941 | return self.cardPagination.getNextUrl() if self.cardPagination is not None else None 942 | def prevPage(self) -> str: 943 | # return self.jsonData['metadata']['prevPage'] if 'prevPage' in self.jsonData['metadata'] else None 944 | return self.cardPagination.getPrevUrl() if self.cardPagination is not None else None 945 | 946 | # HTML 947 | # Make model cards html 948 | def modelCardsHtml(self, models, jsID=0, nsfwLevel=0): 949 | '''Generate HTML of model cards.''' 950 | # NG List 951 | # txtNG = opts.civsfz_ban_creators # Comma-separated text 952 | # txtFav = opts.civsfz_favorite_creators # Comma-separated text 953 | # ngUsers = [s.strip() for s in txtNG.split(",") if s.strip()] 954 | # favUsers = [s.strip() for s in txtFav.split(",") if s.strip()] 955 | cards = [] 956 | for model in models: 957 | index = model[1] 958 | item = self.jsonData['items'][model[1]] 959 | creator = item["creator"]["username"] if "creator" in item else "" 960 | level = item["nsfwLevel"] | (0 if not creator in BanCreators.getAsList() else self.nsfwLevel["Banned"]) 961 | base_model = "" 962 | param = { 963 | "name": item["name"], 964 | "index": index, 965 | "jsId": jsID, 966 | "id": item["id"], 967 | "isNsfw": False, 968 | "nsfwLevel": level, 969 | "matchLevel": self.matchLevel(level, nsfwLevel), 970 | "type": item["type"], 971 | "have": "", 972 | "ea": "", 973 | "imgType": "", 974 | "creator": creator, 975 | "ngUser": creator in BanCreators.getAsList(), 976 | "favorite": creator in FavoriteCreators.getAsList(), 977 | } 978 | if any(item['modelVersions']): 979 | # if len(item['modelVersions'][0]['images']) > 0: 980 | # default image 981 | for i, img in enumerate(item['modelVersions'][0]['images']): 982 | if i == 0: # 0 as default 983 | param['imgType'] = img['type'] 984 | param['imgsrc'] = img["url"] 985 | if img['nsfwLevel'] > 1 and not self.isShowNsfw(): 986 | param['isNsfw'] = True 987 | if self.matchLevel(img['nsfwLevel'], nsfwLevel): 988 | # img = item['modelVersions'][0]['images'][0] 989 | param['imgType'] = img['type'] 990 | param['imgsrc'] = img["url"] 991 | if img['nsfwLevel'] > 1 and not self.isShowNsfw(): 992 | param['isNsfw'] = True 993 | break 994 | base_model = item["modelVersions"][0]['baseModel'] 995 | param['baseModel'] = base_model 996 | 997 | has = self.checkAlreadyHave(index) 998 | if has[0]: 999 | param["have"] = "new" 1000 | elif any(has[1:]) : 1001 | param["have"] = "old" 1002 | 1003 | # ea = item["modelVersions"][0]['earlyAccessDeadline'] if "earlyAccessDeadline" in item["modelVersions"][0] else "" 1004 | ea = item["modelVersions"][0]['availability'] == "EarlyAccess" 1005 | if ea: 1006 | # strEA = item["modelVersions"][0]['earlyAccessDeadline'].replace('Z', '+00:00') # < Python 3.11 1007 | # dtEA = datetime.datetime.fromisoformat(strEA) 1008 | # dtNow = datetime.datetime.now(datetime.timezone.utc) 1009 | # if dtNow < dtEA: 1010 | param['ea'] = 'in' 1011 | cards.append(param) 1012 | 1013 | forTrigger = f'' # for trigger event 1014 | dictBasemodelColor = dictBasemodelColors(self.getBasemodelOptions()) 1015 | template = environment.get_template("cardlist.jinja") 1016 | content = template.render( 1017 | forTrigger=forTrigger, 1018 | cards=cards, 1019 | dictBasemodelColor=dictBasemodelColor, 1020 | cardNoPreview=card_no_preview, 1021 | ) 1022 | return content 1023 | def modelNameTitleHtml(self, name:str, vname:str, base:str, upuser:str="", ea:str=""): 1024 | dictBasemodelColor = dictBasemodelColors(self.getBasemodelOptions()) 1025 | template = environment.get_template("modelTitle.jinja") 1026 | content = template.render( 1027 | modelName=name, versionName=vname, baseModel=base, uploadUser=upuser, dictBasemodelColor=dictBasemodelColor, ea=ea, 1028 | ) 1029 | return content 1030 | 1031 | def meta2html(self, meta:dict) -> str: 1032 | # convert key name as infotext 1033 | sortKey = [ 1034 | 'prompt', 1035 | 'negativePrompt', 1036 | 'Model', 1037 | 'VAE', 1038 | 'sampler', 1039 | 'Schedule type', 1040 | 'cfgScale', 1041 | 'steps', 1042 | 'seed', 1043 | 'clipSkip', 1044 | ] 1045 | infotextDict = { key: meta[key] if key in meta else None for key in sortKey } 1046 | infotextDict.update(meta) 1047 | # print(f"{infotextDict=}") 1048 | # if 'hashes' in infotextDict: 1049 | # print(type(infotextDict['hashes'])) 1050 | 1051 | template = environment.get_template("infotext.jinja") 1052 | content = template.render(infotext=infotextDict) 1053 | return content 1054 | 1055 | def meta2infotext(self, meta:dict) -> str: 1056 | # convert key name as infotext 1057 | renameKey = { 1058 | 'prompt':'Prompt', 1059 | 'negativePrompt': 'Negative prompt', 1060 | 'sampler': 'Sampler', 1061 | 'steps': 'Steps', 1062 | 'seed': 'Seed', 1063 | 'cfgScale': 'CFG scale', 1064 | 'clipSkip': 'Clip skip' 1065 | } 1066 | infotextDict = {renameKey.get(key, key): value for key, value in meta.items()} if meta is not None else None 1067 | # print(f"{infotextDict=}") 1068 | infotext = "" 1069 | if 'Prompt' in infotextDict: 1070 | infotext += infotextDict['Prompt'] + "\n" 1071 | if 'Negative prompt' in infotextDict: 1072 | infotext += "Negative prompt: " + infotextDict['Negative prompt'] 1073 | infotext += "\n" 1074 | tmpList:list = [] 1075 | for key, value in infotextDict.items(): 1076 | if not key in ('Prompt','Negative prompt'): 1077 | tmpList.append("{}:{}".format(key,value)) 1078 | infotext += ",".join(tmpList) 1079 | return infotext 1080 | 1081 | def modelInfoHtml(self, modelInfo:dict, nsfwLevel:int=0) -> str: 1082 | '''Generate HTML of model info''' 1083 | samples = "" 1084 | for pic in modelInfo["modelVersions"][0]["images"]: 1085 | if self.matchLevel(pic['nsfwLevel'], nsfwLevel): 1086 | nsfw = pic['nsfwLevel'] > 1 and not self.showNsfw 1087 | infotext = self.meta2infotext(pic['meta']) if pic['meta'] is not None else "" 1088 | metaHtml = self.meta2html(pic['meta']) if pic['meta'] is not None else "" 1089 | template = environment.get_template("sampleImage.jinja") 1090 | samples += template.render( 1091 | pic=pic, 1092 | nsfw=nsfw, 1093 | infotext=infotext, 1094 | metaHtml=metaHtml 1095 | ) 1096 | filesize = modelInfo["modelVersions"][0]["files"][0]["sizeKB"] 1097 | # Get primary file size 1098 | fileIndex = 0 1099 | for i, file in enumerate(modelInfo["modelVersions"][0]["files"]): 1100 | if "primary" in file: 1101 | fileIndex = i 1102 | 1103 | # created = self.getCreatedDatetime().astimezone( 1104 | # tz.tzlocal()).replace(microsecond=0).isoformat() 1105 | if self.getPublishedDatetime() is not None: 1106 | published = self.getPublishedDatetime().astimezone( 1107 | tz.tzlocal()).replace(microsecond=0).isoformat() 1108 | else: 1109 | published = "" 1110 | # updated = self.getUpdatedDatetime().astimezone( 1111 | # tz.tzlocal()).replace(microsecond=0).isoformat() 1112 | dictBasemodelColor = dictBasemodelColors(self.getBasemodelOptions()) 1113 | template = environment.get_template("modelbasicinfo.jinja") 1114 | basicInfo = template.render( 1115 | modelInfo=modelInfo, 1116 | published=published, 1117 | strNsfw=self.strNsfwLevel(modelInfo["nsfwLevel"]), 1118 | strVNsfw=self.strNsfwLevel(modelInfo["modelVersions"][0]["nsfwLevel"]), 1119 | fileIndex=fileIndex, 1120 | dictBasemodelColor=dictBasemodelColor, 1121 | ) 1122 | 1123 | permissions = self.permissionsHtml(self.allows2permissions()) 1124 | # function:copy to clipboard 1125 | js = ( 1126 | "" 1144 | ) 1145 | template = environment.get_template("modelInfo.jinja") 1146 | content = template.render( 1147 | modelInfo=modelInfo, basicInfo=basicInfo, permissions=permissions, 1148 | samples=samples, js=js) 1149 | 1150 | return content 1151 | 1152 | def permissionsHtml(self, premissions:dict, msgType:int=3) -> str: 1153 | template = environment.get_template("permissions.jinja") 1154 | content = template.render(premissions) 1155 | return content 1156 | 1157 | # REST API 1158 | def makeRequestQuery(self, content_type, sort_type, period, search_type, base_models=None, grChkboxShowNsfw=False, 1159 | grDrpdwnKeyword="", 1160 | grDrpdwnUserName="", 1161 | grDrpdwnTag="", 1162 | grDrpdwnID="", 1163 | grchkbxfav="" 1164 | ): 1165 | if grDrpdwnID is not None: 1166 | grDrpdwnID = str.strip(grDrpdwnID) 1167 | if "Model ID" in search_type or "Version ID" in search_type: 1168 | if not grDrpdwnID.isdecimal(): 1169 | query = "" 1170 | print_ly(f'"{grDrpdwnID}" is not a numerical value') 1171 | else: 1172 | query = str.strip(grDrpdwnID) 1173 | elif "Hash" in search_type: 1174 | try: 1175 | int("0x" + grDrpdwnID, 16) 1176 | isHex = True 1177 | except ValueError: 1178 | isHex = False 1179 | if isHex: 1180 | query = str.strip(grDrpdwnID) 1181 | else: 1182 | query = "" 1183 | print_ly(f'"{grDrpdwnID}" is not a hexadecimal value') 1184 | else: 1185 | query = {'types': content_type, 'sort': sort_type, 1186 | 'limit': opts.civsfz_number_of_cards, 'page': 1, 'nsfw': grChkboxShowNsfw} 1187 | if not period == "AllTime": 1188 | query |= {'period': period} 1189 | if "User name" in search_type: 1190 | query |= {'username': grDrpdwnUserName } 1191 | if "Tag" in search_type: 1192 | query |= {'tag': grDrpdwnTag } 1193 | if "Keyword" in search_type: 1194 | query |= {"query": grDrpdwnKeyword} 1195 | query.pop("page", None) # Cannot use page param with query search 1196 | if base_models: 1197 | query |= {'baseModels': base_models } 1198 | if grchkbxfav: 1199 | query |= {"favorites": grchkbxfav} 1200 | return query 1201 | 1202 | def updateQuery(self, url:str , addQuery:dict) -> str: 1203 | parse = urllib.parse.urlparse(url) 1204 | strQuery = parse.query 1205 | dictQuery = urllib.parse.parse_qs(strQuery) 1206 | query = dictQuery | addQuery 1207 | newURL = parse._replace(query=urllib.parse.urlencode(query, doseq=True, quote_via=urllib.parse.quote)) 1208 | return urllib.parse.urlunparse(newURL) 1209 | 1210 | def requestApi(self, url=None, query=None, timeout=read_timeout()): 1211 | self.requestError = None 1212 | hasFavorites = "favorites" in query if query else False 1213 | if url is None: 1214 | url = self.getModelsApiUrl() 1215 | if query is not None: 1216 | # Replace Boolean with a lowercase string True->true, False->false 1217 | query = { k: str(v).lower() if isinstance(v, bool) else v 1218 | for k, v in query.items()} 1219 | # print_lc(f'{query=}') 1220 | query = urllib.parse.urlencode(query, doseq=True, quote_via=urllib.parse.quote) 1221 | 1222 | # Make a GET request to the API 1223 | # cachePath = Path.joinpath(extensionFolder(), "../api_cache") 1224 | # headers = {'Cache-Control': 'no-cache'} if cache else {} 1225 | try: 1226 | # with CachedSession(cache_name=cachePath.resolve(), expire_after=5*60) as session: 1227 | browse = Browser() 1228 | if hasFavorites: 1229 | api_key=getattr(opts,"civsfz_api_key", None) 1230 | if api_key is None: 1231 | print_ly(f"No API Key.") 1232 | else: 1233 | browse.setAPIKey(api_key) 1234 | response = browse.session.get( 1235 | url, params=query, timeout=read_timeout() 1236 | ) 1237 | # print_lc(f'{response.url=}') 1238 | # print_lc(f'Page cache: {response.headers["CF-Cache-Status"]}') 1239 | response.raise_for_status() 1240 | except requests.exceptions.RequestException as e: 1241 | # print(Fore.LIGHTYELLOW_EX + "Request error: " , e) 1242 | # print(Style.RESET_ALL) 1243 | print_ly(f"Request error: {e}") 1244 | # print(f"{response=}") 1245 | browse.reConnect() 1246 | data = self.jsonData # No update data 1247 | self.requestError = e 1248 | else: 1249 | # print_lc(f'{response.url=}') 1250 | response.encoding = "utf-8" # response.apparent_encoding 1251 | data = json.loads(response.text) 1252 | data['requestUrl'] = response.url 1253 | data = self.patchResponse(data) 1254 | return data 1255 | 1256 | def patchResponse(self, data:dict) -> dict: 1257 | # make compatibility 1258 | # if 'metadata' in data: 1259 | # print_lc(f"{data['metadata']=}") 1260 | # parse = urllib.parse.urlparse(data['metadata']['nextPage']) 1261 | # strQuery = parse.query 1262 | # dictQuery = urllib.parse.parse_qs(strQuery) 1263 | # dictQuery.pop('cursor', None) 1264 | # addQuery = { 'page': data['metadata']['currentPage']} 1265 | # query = dictQuery | addQuery 1266 | # currentURL = parse._replace(query=urllib.parse.urlencode(query, doseq=True, quote_via=urllib.parse.quote)) 1267 | # data['metadata']['currentPageUrl'] = urllib.parse.urlunparse(currentURL) 1268 | # if data['metadata']['currentPage'] == data['metadata']['pageSize']: 1269 | # data['metadata']['nextPage'] = None 1270 | # if data['metadata']['currentPage'] < data['metadata']['pageSize']: 1271 | # addQuery ={ 'page': data['metadata']['currentPage'] + 1 } 1272 | # query = dictQuery | addQuery 1273 | # query.pop('cursor', None) 1274 | # nextPage = parse._replace(query=urllib.parse.urlencode(query, doseq=True, quote_via=urllib.parse.quote)) 1275 | # data['metadata']['nextPage'] = urllib.parse.urlunparse(nextPage) 1276 | ##if data['metadata']['currentPage'] > 1: 1277 | # addQuery ={ 'page': data['metadata']['currentPage'] - 1 } 1278 | # query = dictQuery | addQuery 1279 | # query.pop('cursor', None) 1280 | # prevURL = parse._replace(query=urllib.parse.urlencode(query, doseq=True, quote_via=urllib.parse.quote)) 1281 | # data['metadata']['prevPage'] = prevURL 1282 | 1283 | return data 1284 | 1285 | def requestImagesByVersionId(self, versionId=None, limit=None): 1286 | if versionId == None: 1287 | return None 1288 | params = {"modelVersionId": versionId, 1289 | "sort": "Oldest", 1290 | 'nsfw': 'X'} 1291 | if limit is not None: 1292 | params |= {"limit": limit} 1293 | return self.requestApi(self.getImagesApiUrl(), params, timeout=read_timeout()) 1294 | def requestVersionByVersionID(self, versionID=None): 1295 | if versionID == None: 1296 | return None 1297 | url = self.getVersionsApiUrl(versionID) 1298 | ret = self.requestApi(url, timeout=read_timeout()) 1299 | if self.requestError is not None: 1300 | ret = None 1301 | return ret 1302 | --------------------------------------------------------------------------------