├── .gitignore ├── DEVELOPING.md ├── LICENSE ├── README.md ├── civitai ├── generation.py ├── hashes.py ├── lib.py ├── link.py └── models.py ├── install.py ├── javascript ├── across-tabs.min.js └── civitai.js ├── preload.py ├── requirements.txt ├── scripts ├── api.py ├── gen_hashing.py ├── info.py ├── link.py ├── pasted.py ├── previews.py └── settings.py └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | /repositories 3 | /venv 4 | /outputs 5 | /log 6 | /webui.settings.bat 7 | /.idea 8 | .vscode 9 | /test/stdout.txt 10 | /test/stderr.txt 11 | -------------------------------------------------------------------------------- /DEVELOPING.md: -------------------------------------------------------------------------------- 1 | I've been referencing this extension repo as a guide: 2 | [DreamBooth Extension](https://github.com/d8ahazard/sd_dreambooth_extension/blob/main/scripts/main.py) 3 | [stable-diffusion-webui-inspiration](https://github.com/yfszzx/stable-diffusion-webui-inspiration) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Civitai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Civitai Extension for Automatic 1111 Stable Diffusion Web UI 2 | 3 | Manage and interact with your Automatic 1111 SD instance right from Civitai 4 | 5 | https://user-images.githubusercontent.com/607609/234462691-ecd578cc-b0ec-49e4-8e50-a917395a6874.mp4 6 | 7 | ## Features 8 | - [x] Automatically download preview images for all models, LORAs, hypernetworks, and embeds 9 | - [x] Automatically download a model based on the model hash upon applying pasted generation params 10 | - [x] Resources in Metadata: Include the SHA256 hash of all resources used in an image to be able to automatically link to corresponding resources on Civitai 11 | - [x] Flexible Resource Naming in Metadata: Hashify the names of resources in prompts to avoid issues with resource renaming and make prompts more portable 12 | - [x] **[Civitai Link (Alpha)](https://civitai.com/v/civitai-link-intro):** Optional websocket connection to be able to add/remove resources and more in your SD instance while browsing Civitai or other Civitai Link enabled sites. 13 | 14 | ## Installation 15 | 16 | ### Through the Extensions UI (Recommended) 17 | 1. Open the Extensions Tab in Automatic1111 SD Web UI 18 | 2. In the Extension Tab Open the "Instal from URL" tab 19 | 3. Paste `https://github.com/civitai/sd_civitai_extension.git` into the URL input 20 | 4. Press install and wait for it to complete 21 | 5. **Restart Automatic1111** (Reloading the UI will not install the necessary requirements) 22 | 23 | ***note:*** for the correct usage of this extension, make sure to **remove** the `--no-hashing` launch option from Automatic 1111 (if you are using it) as this extension relies on hash information provided it. 24 | 25 | ### Manually 26 | 1. Download the repo using any method (zip download or cloning) 27 | ```sh 28 | git clone https://github.com/civitai/sd_civitai_extension.git 29 | ``` 30 | 31 | 2. After downloading the repo, open a command prompt in that location 32 | ```sh 33 | cd C:\path\to\sd_civitai_extension 34 | ``` 35 | 36 | 3. Then run the included install.py script 37 | ```sh 38 | py install.py 39 | # OR 40 | python install.py 41 | ``` 42 | 43 | ***note:*** for the correct usage of this extension, make sure to **remove** the `--no-hashing` launch option from Automatic 1111 (if you are using it) as this extension relies on hash information provided it. 44 | 45 | ## Frequently Asked Questions 46 | 47 | ### What the Civitai Link Key? Where do I get it? 48 | The Civitai Link Key is a short 6 character token that you'll receive when setting up your Civitai Link instance (you can see it referenced here in this [Civitai Link installation video](https://civitai.com/v/civitai-link-installation)). The Link Key acts as a temporary secret key to connect your Stable Diffusion instance to your Civitai Account inside our link service. 49 | 50 | Since Civitai Link is still in alpha, it is currently only available to Supporters as part of the Civitai Early Access program. You can get access to Civitai Link today by [becoming a supporter](https://civitai.com/pricing) 🥰 or you can wait until we've gotten it to a state that we're ready for a full release. 51 | 52 | ## Contribute 53 | 54 | Hop into the development channel in our [Discord server](https://discord.gg/UwX5wKwm6c) and let's chat! 55 | -------------------------------------------------------------------------------- /civitai/generation.py: -------------------------------------------------------------------------------- 1 | # api endpoints 2 | from modules import shared 3 | from modules.api.api import encode_pil_to_base64, validate_sampler_name 4 | from modules.api.models import StableDiffusionTxt2ImgProcessingAPI, TextToImageResponse 5 | from modules.processing import StableDiffusionProcessingTxt2Img, process_images 6 | from modules.call_queue import queue_lock 7 | 8 | from civitai.models import CommandImageTxt2Img 9 | import civitai.lib as lib 10 | 11 | def internal_txt2img(txt2imgreq: StableDiffusionTxt2ImgProcessingAPI): 12 | populate = txt2imgreq.copy(update={ # Override __init__ params 13 | "sampler_name": validate_sampler_name(txt2imgreq.sampler_name or txt2imgreq.sampler_index), 14 | "do_not_save_samples": True, 15 | "do_not_save_grid": True 16 | } 17 | ) 18 | if populate.sampler_name: 19 | populate.sampler_index = None # prevent a warning later on 20 | 21 | args = vars(populate) 22 | args.pop('script_name', None) 23 | args.pop('script_args', None) # will refeed them to the pipeline directly after initializing them 24 | args.pop('alwayson_scripts', None) 25 | 26 | send_images = args.pop('send_images', True) 27 | args.pop('save_images', None) 28 | 29 | with queue_lock: 30 | p = StableDiffusionProcessingTxt2Img(sd_model=shared.sd_model, **args) 31 | 32 | shared.state.begin() 33 | processed = process_images(p) 34 | shared.state.end() 35 | 36 | b64images = list(map(encode_pil_to_base64, processed.images)) if send_images else [] 37 | 38 | return TextToImageResponse(images=b64images, parameters=vars(txt2imgreq), info=processed.js()) 39 | 40 | def txt2img(command: CommandImageTxt2Img): 41 | # TODO: Add support for VAEs 42 | # if (command['vae'] is None): lib.clear_vae() 43 | 44 | if (command['model'] is not None): lib.select_model({ 'hash': command['model'] }) 45 | # if (command['vae'] is not None): lib.select_vae(command['vae']) 46 | 47 | return internal_txt2img( 48 | StableDiffusionTxt2ImgProcessingAPI( 49 | prompt=command['params']['prompt'], 50 | negative_prompt=command['params']['negativePrompt'], 51 | seed=command['params']['seed'], 52 | steps=command['params']['steps'], 53 | width=command['params']['width'], 54 | height=command['params']['height'], 55 | cfg_scale=command['params']['cfgScale'], 56 | clip_skip=command['params']['clipSkip'], 57 | n_iter=command['quantity'], 58 | batch_size=command['batchSize'], 59 | ) 60 | ) -------------------------------------------------------------------------------- /civitai/hashes.py: -------------------------------------------------------------------------------- 1 | # from blake3 import blake3 as hasher 2 | import os.path 3 | 4 | from modules import hashes as sd_hashes 5 | 6 | # NOTE: About this file 7 | # --------------------------------- 8 | # This is not being used. It was added to see if Blake3 was faster than SHA256. 9 | # It was not noticeably faster in our tests, so we are sticking with SHA256 for now. 10 | # Especially since SHA256 is the standard inside this UI. 11 | 12 | # cache_key = "civitai_hashes" 13 | 14 | # def blake3_from_cache(filename, title): 15 | # hashes = sd_hashes.cache(cache_key) 16 | # ondisk_mtime = os.path.getmtime(filename) 17 | 18 | # if title not in hashes: 19 | # return None 20 | 21 | # cached_blake3 = hashes[title].get("blake3", None) 22 | # cached_mtime = hashes[title].get("mtime", 0) 23 | 24 | # if ondisk_mtime > cached_mtime or cached_blake3 is None: 25 | # return None 26 | 27 | # return cached_blake3 28 | 29 | # def calculate_blake3(filename): 30 | # hash_blake3 = hasher() 31 | # blksize = 1024 * 1024 32 | 33 | # with open(filename, "rb") as f: 34 | # for chunk in iter(lambda: f.read(blksize), b""): 35 | # hash_blake3.update(chunk) 36 | 37 | # return hash_blake3.hexdigest() 38 | 39 | # def blake3(filename, title): 40 | # hashes = sd_hashes.cache(cache_key) 41 | 42 | # blake3_value = blake3_from_cache(filename, title) 43 | # if blake3_value is not None: 44 | # return blake3_value 45 | 46 | # print(f"Calculating blake3 for {filename}: ", end='') 47 | # blake3_value = calculate_blake3(filename) 48 | # print(f"{blake3_value}") 49 | 50 | # if title not in hashes: 51 | # hashes[title] = {} 52 | 53 | # hashes[title]["mtime"] = os.path.getmtime(filename) 54 | # hashes[title]["blake3"] = blake3_value 55 | 56 | # sd_hashes.dump_cache() 57 | 58 | # return blake3_value 59 | -------------------------------------------------------------------------------- /civitai/lib.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import shutil 4 | import tempfile 5 | import time 6 | from typing import List 7 | import requests 8 | import glob 9 | 10 | from tqdm import tqdm 11 | from modules import shared, sd_models, sd_vae, hashes 12 | from modules.ui_extra_networks import extra_pages 13 | from modules.paths import models_path 14 | from civitai.models import Command, ResourceRequest 15 | 16 | #region shared variables 17 | try: 18 | base_url = shared.cmd_opts.civitai_endpoint 19 | except: 20 | base_url = 'https://civitai.com/api/v1' 21 | 22 | connected = False 23 | user_agent = 'CivitaiLink:Automatic1111' 24 | download_chunk_size = 8192 25 | cache_key = 'civitai' 26 | refresh_previews_function = None 27 | refresh_info_function = None 28 | #endregion 29 | 30 | #region Utils 31 | def log(message): 32 | """Log a message to the console.""" 33 | print(f'Civitai: {message}') 34 | 35 | def download_file(url, dest, on_progress=None): 36 | if os.path.exists(dest): 37 | log(f'File already exists: {dest}') 38 | 39 | log(f'Downloading: "{url}" to {dest}\n') 40 | 41 | response = requests.get(url, stream=True, headers={"User-Agent": user_agent}) 42 | total = int(response.headers.get('content-length', 0)) 43 | start_time = time.time() 44 | 45 | dest = os.path.expanduser(dest) 46 | dst_dir = os.path.dirname(dest) 47 | f = tempfile.NamedTemporaryFile(delete=False, dir=dst_dir) 48 | 49 | try: 50 | current = 0 51 | with tqdm(total=total, unit='B', unit_scale=True, unit_divisor=1024) as bar: 52 | for data in response.iter_content(chunk_size=download_chunk_size): 53 | current += len(data) 54 | pos = f.write(data) 55 | bar.update(pos) 56 | if on_progress is not None: 57 | should_stop = on_progress(current, total, start_time) 58 | if should_stop == True: 59 | raise Exception('Download cancelled') 60 | f.close() 61 | shutil.move(f.name, dest) 62 | except OSError as e: 63 | print(f"Could not write the preview file to {dst_dir}") 64 | print(e) 65 | finally: 66 | f.close() 67 | if os.path.exists(f.name): 68 | os.remove(f.name) 69 | #endregion Utils 70 | 71 | #region API 72 | def req(endpoint, method='GET', data=None, params=None, headers=None): 73 | """Make a request to the Civitai API.""" 74 | if headers is None: 75 | headers = {} 76 | headers['User-Agent'] = user_agent 77 | api_key = shared.opts.data.get("civitai_api_key", None) 78 | if api_key is not None: 79 | headers['Authorization'] = f'Bearer {api_key}' 80 | if data is not None: 81 | headers['Content-Type'] = 'application/json' 82 | data = json.dumps(data) 83 | if not endpoint.startswith('/'): 84 | endpoint = '/' + endpoint 85 | if params is None: 86 | params = {} 87 | response = requests.request(method, base_url+endpoint, data=data, params=params, headers=headers) 88 | if response.status_code != 200: 89 | raise Exception(f'Error: {response.status_code} {response.text}') 90 | return response.json() 91 | 92 | def get_models(query, creator, tag, type, page=1, page_size=20, sort='Most Downloaded', period='AllTime'): 93 | """Get a list of models from the Civitai API.""" 94 | response = req('/models', params={ 95 | 'query': query, 96 | 'username': creator, 97 | 'tag': tag, 98 | 'type': type, 99 | 'sort': sort, 100 | 'period': period, 101 | 'page': page, 102 | 'pageSize': page_size, 103 | }) 104 | return response 105 | 106 | def get_all_by_hash(hashes: List[str]): 107 | response = req(f"/model-versions/by-hash", method='POST', data=hashes) 108 | return response 109 | 110 | def get_model_version(id): 111 | """Get a model version from the Civitai API.""" 112 | response = req('/model-versions/'+id) 113 | return response 114 | 115 | def get_model_version_by_hash(hash: str): 116 | response = req(f"/model-versions/by-hash/{hash}") 117 | return response 118 | 119 | def get_creators(query, page=1, page_size=20): 120 | """Get a list of creators from the Civitai API.""" 121 | response = req('/creators', params={ 122 | 'query': query, 123 | 'page': page, 124 | 'pageSize': page_size 125 | }) 126 | return response 127 | 128 | def get_tags(query, page=1, page_size=20): 129 | """Get a list of tags from the Civitai API.""" 130 | response = req('/tags', params={ 131 | 'query': query, 132 | 'page': page, 133 | 'pageSize': page_size 134 | }) 135 | return response 136 | #endregion API 137 | 138 | #region Get Utils 139 | def get_lora_dir(): 140 | lora_dir = shared.opts.data.get('civitai_folder_lora', shared.cmd_opts.lora_dir).strip() 141 | if not lora_dir: lora_dir = shared.cmd_opts.lora_dir 142 | return lora_dir 143 | 144 | def get_locon_dir(): 145 | try: 146 | lyco_dir = shared.opts.data.get('civitai_folder_lyco', shared.cmd_opts.lyco_dir).strip() 147 | if not lyco_dir: lyco_dir = shared.cmd_opts.lyco_dir 148 | if not lyco_dir: lyco_dir = os.path.join(models_path, "LyCORIS"), 149 | return lyco_dir 150 | except: 151 | return get_lora_dir() 152 | 153 | def get_model_dir(): 154 | model_dir = shared.opts.data.get('civitai_folder_model', shared.cmd_opts.ckpt_dir) 155 | if not model_dir: model_dir = shared.cmd_opts.ckpt_dir 156 | if not model_dir: model_dir = sd_models.model_path 157 | return model_dir.strip() 158 | 159 | def get_automatic_type(type: str): 160 | if type == 'Hypernetwork': return 'hypernet' 161 | return type.lower() 162 | 163 | def get_automatic_name(type: str, filename: str, folder: str): 164 | abspath = os.path.abspath(filename) 165 | if abspath.startswith(folder): 166 | fullname = abspath.replace(folder, '') 167 | else: 168 | fullname = os.path.basename(filename) 169 | 170 | if fullname.startswith("\\") or fullname.startswith("/"): 171 | fullname = fullname[1:] 172 | 173 | if type == 'Checkpoint': return fullname 174 | return os.path.splitext(fullname)[0] 175 | 176 | def has_preview(filename: str): 177 | preview_exts = [".jpg", ".png", ".jpeg", ".gif"] 178 | preview_exts = [*preview_exts, *[".preview" + x for x in preview_exts]] 179 | for ext in preview_exts: 180 | if os.path.exists(os.path.splitext(filename)[0] + ext): 181 | return True 182 | return False 183 | 184 | def has_info(filename: str): 185 | return os.path.isfile(os.path.splitext(filename)[0] + '.json') 186 | 187 | def get_resources_in_folder(type, folder, exts=[], exts_exclude=[]): 188 | resources = [] 189 | os.makedirs(folder, exist_ok=True) 190 | 191 | candidates = [] 192 | for ext in exts: 193 | candidates += glob.glob(os.path.join(folder, '**/*.' + ext), recursive=True) 194 | for ext in exts_exclude: 195 | candidates = [x for x in candidates if not x.endswith(ext)] 196 | 197 | folder = os.path.abspath(folder) 198 | automatic_type = get_automatic_type(type) 199 | for filename in sorted(candidates): 200 | if os.path.isdir(filename): 201 | continue 202 | 203 | name = os.path.splitext(os.path.basename(filename))[0] 204 | automatic_name = get_automatic_name(type, filename, folder) 205 | hash = hashes.sha256(filename, f"{automatic_type}/{automatic_name}") 206 | 207 | resources.append({'type': type, 'name': name, 'hash': hash, 'path': filename, 'hasPreview': has_preview(filename), 'hasInfo': has_info(filename) }) 208 | 209 | return resources 210 | 211 | resources = [] 212 | def load_resource_list(types=['LORA', 'LoCon', 'Hypernetwork', 'TextualInversion', 'Checkpoint', 'VAE', 'Controlnet', 'Upscaler']): 213 | global resources 214 | 215 | # If resources is empty and types is empty, load all types 216 | # This is a helper to be able to get the resource list without 217 | # having to worry about initialization. On subsequent calls, no work will be done 218 | if len(resources) == 0 and len(types) == 0: 219 | types = ['LORA', 'LoCon', 'Hypernetwork', 'TextualInversion', 'Checkpoint', 'VAE', 'Controlnet', 'Upscaler'] 220 | 221 | if 'LORA' in types: 222 | resources = [r for r in resources if r['type'] != 'LORA'] 223 | resources += get_resources_in_folder('LORA', get_lora_dir(), ['pt', 'safetensors', 'ckpt']) 224 | if 'LoCon' in types: 225 | resources = [r for r in resources if r['type'] != 'LoCon'] 226 | resources += get_resources_in_folder('LoCon', get_locon_dir(), ['pt', 'safetensors', 'ckpt']) 227 | if 'Hypernetwork' in types: 228 | resources = [r for r in resources if r['type'] != 'Hypernetwork'] 229 | resources += get_resources_in_folder('Hypernetwork', shared.cmd_opts.hypernetwork_dir, ['pt', 'safetensors', 'ckpt']) 230 | if 'TextualInversion' in types: 231 | resources = [r for r in resources if r['type'] != 'TextualInversion'] 232 | resources += get_resources_in_folder('TextualInversion', shared.cmd_opts.embeddings_dir, ['pt', 'bin', 'safetensors']) 233 | if 'Checkpoint' in types: 234 | resources = [r for r in resources if r['type'] != 'Checkpoint'] 235 | resources += get_resources_in_folder('Checkpoint', get_model_dir(), ['safetensors', 'ckpt'], ['vae.safetensors', 'vae.ckpt']) 236 | if 'Controlnet' in types: 237 | resources = [r for r in resources if r['type'] != 'Controlnet'] 238 | resources += get_resources_in_folder('Controlnet', os.path.join(models_path, "ControlNet"), ['safetensors', 'ckpt'], ['vae.safetensors', 'vae.ckpt']) 239 | if 'Upscaler' in types: 240 | resources = [r for r in resources if r['type'] != 'Upscaler'] 241 | resources += get_resources_in_folder('Upscaler', os.path.join(models_path, "ESRGAN"), ['safetensors', 'ckpt', 'pt']) 242 | if 'VAE' in types: 243 | resources = [r for r in resources if r['type'] != 'VAE'] 244 | resources += get_resources_in_folder('VAE', get_model_dir(), ['vae.pt', 'vae.safetensors', 'vae.ckpt']) 245 | resources += get_resources_in_folder('VAE', sd_vae.vae_path, ['pt', 'safetensors', 'ckpt']) 246 | 247 | return resources 248 | 249 | def get_resource_by_hash(hash: str): 250 | resources = load_resource_list([]) 251 | 252 | found = [resource for resource in resources if hash.lower() == resource['hash'] and ('downloading' not in resource or resource['downloading'] != True)] 253 | if found: 254 | return found[0] 255 | 256 | return None 257 | 258 | def get_model_by_hash(hash: str): 259 | found = [info for info in sd_models.checkpoints_list.values() if hash == info.sha256 or hash == info.shorthash or hash == info.hash] 260 | if found: 261 | return found[0] 262 | 263 | return None 264 | 265 | #endregion Get Utils 266 | 267 | #region Removing 268 | def remove_resource(resource: ResourceRequest): 269 | removed = None 270 | target = get_resource_by_hash(resource['hash']) 271 | if target is None or target['type'] != resource['type']: removed = False 272 | elif os.path.exists(target['path']): 273 | os.remove(target['path']) 274 | removed = True 275 | 276 | if removed == True: 277 | log(f'Removed resource') 278 | load_resource_list([resource['type']]) 279 | if resource['type'] == 'Checkpoint': 280 | sd_models.list_models() 281 | elif resource['type'] == 'Hypernetwork': 282 | shared.reload_hypernetworks() 283 | # elif resource['type'] == 'LORA': 284 | # TODO: reload LORA 285 | elif removed == None: 286 | log(f'Resource not found') 287 | #endregion Removing 288 | 289 | #region Downloading 290 | def load_if_missing(path, url, on_progress=None): 291 | if os.path.exists(path): return True 292 | if url is None: return False 293 | 294 | download_file(url, path, on_progress) 295 | return None 296 | 297 | def load_resource(resource: ResourceRequest, on_progress=None): 298 | resource['hash'] = resource['hash'].lower() 299 | existing_resource = get_resource_by_hash(resource['hash']) 300 | if existing_resource: 301 | log(f'Already have resource: {resource["name"]}') 302 | return 303 | 304 | resources.append({'type': resource['type'], 'name': resource['name'], 'hash': resource['hash'], 'downloading': True }) 305 | 306 | if resource['type'] == 'Checkpoint': load_model(resource, on_progress) 307 | elif resource['type'] == 'CheckpointConfig': load_model_config(resource, on_progress) 308 | elif resource['type'] == 'Controlnet': load_controlnet(resource, on_progress) 309 | elif resource['type'] == 'Upscaler': load_upscaler(resource, on_progress) 310 | elif resource['type'] == 'Hypernetwork': load_hypernetwork(resource, on_progress) 311 | elif resource['type'] == 'TextualInversion': load_textual_inversion(resource, on_progress) 312 | elif resource['type'] == 'LORA': load_lora(resource, on_progress) 313 | elif resource['type'] == 'LoCon': load_locon(resource, on_progress) 314 | 315 | load_resource_list([resource['type']]) 316 | 317 | def fetch_model_by_hash(hash: str): 318 | model_version = get_model_version_by_hash(hash) 319 | if model_version is None: 320 | log(f'Could not find model version with hash {hash}') 321 | return None 322 | 323 | file = [x for x in model_version['files'] if x['primary'] == True][0] 324 | resource = ResourceRequest( 325 | name=file['name'], 326 | type='Checkpoint', 327 | hash=file['hashes']['SHA256'], 328 | url=file['downloadUrl'] 329 | ) 330 | load_resource(resource) 331 | 332 | def load_model_config(resource: ResourceRequest, on_progress=None): 333 | load_if_missing(os.path.join(get_model_dir(), resource['name']), resource['url'], on_progress) 334 | 335 | def load_model(resource: ResourceRequest, on_progress=None): 336 | model = get_model_by_hash(resource['hash']) 337 | if model is not None: 338 | log('Found model in model list') 339 | if model is None and resource['url'] is not None: 340 | log('Downloading model') 341 | download_file(resource['url'], os.path.join(get_model_dir(), resource['name']), on_progress) 342 | sd_models.list_models() 343 | model = get_model_by_hash(resource['hash']) 344 | 345 | return model 346 | 347 | def load_controlnet(resource: ResourceRequest, on_progress=None): 348 | isAvailable = load_if_missing(os.path.join(models_path, 'ControlNet', resource['name']), resource['url'], on_progress) 349 | # TODO: reload controlnet list - not sure best way to import this 350 | # if isAvailable is None: 351 | # controlnet.list_available_models() 352 | 353 | def load_upscaler(resource: ResourceRequest, on_progress=None): 354 | isAvailable = load_if_missing(os.path.join(models_path, 'ESRGAN', resource['name']), resource['url'], on_progress) 355 | # TODO: reload upscaler list - not sure best way to import this 356 | # if isAvailable is None: 357 | # upscaler.list_available_models() 358 | 359 | def load_textual_inversion(resource: ResourceRequest, on_progress=None): 360 | load_if_missing(os.path.join(shared.cmd_opts.embeddings_dir, resource['name']), resource['url'], on_progress) 361 | 362 | def load_lora(resource: ResourceRequest, on_progress=None): 363 | isAvailable = load_if_missing(os.path.join(get_lora_dir(), resource['name']), resource['url'], on_progress) 364 | if isAvailable is None: 365 | page = next(iter([x for x in extra_pages if x.name == "lora"]), None) 366 | if page is not None: 367 | log('Refreshing Loras') 368 | page.refresh() 369 | if refresh_previews_function is not None: 370 | refresh_previews_function() 371 | if refresh_info_function is not None: 372 | refresh_info_function() 373 | 374 | def load_locon(resource: ResourceRequest, on_progress=None): 375 | isAvailable = load_if_missing(os.path.join(get_locon_dir(), resource['name']), resource['url'], on_progress) 376 | if isAvailable is None: 377 | page = next(iter([x for x in extra_pages if x.name == "lora"]), None) 378 | if page is not None: 379 | log('Refreshing Locons') 380 | page.refresh() 381 | 382 | def load_vae(resource: ResourceRequest, on_progress=None): 383 | # TODO: find by hash instead of name 384 | if not resource['name'].endswith('.pt'): resource['name'] += '.pt' 385 | full_path = os.path.join(models_path, 'VAE', resource['name']) 386 | 387 | isAvailable = load_if_missing(full_path, resource['url'], on_progress) 388 | if isAvailable is None: 389 | sd_vae.refresh_vae_list() 390 | 391 | def load_hypernetwork(resource: ResourceRequest, on_progress=None): 392 | full_path = os.path.join(shared.cmd_opts.hypernetwork_dir, resource['name']); 393 | if not full_path.endswith('.pt'): full_path += '.pt' 394 | isAvailable = load_if_missing(full_path, resource['url'], on_progress) 395 | if isAvailable is None: 396 | shared.reload_hypernetworks() 397 | 398 | #endregion Downloading 399 | 400 | #region Selecting Resources 401 | def select_model(resource: ResourceRequest): 402 | if shared.opts.data["sd_checkpoint_hash"] == resource['hash']: return 403 | 404 | model = load_model(resource) 405 | 406 | if shared.sd_model.sd_checkpoint_info.model_name == model.model_name: 407 | return 408 | if model is not None: 409 | sd_models.load_model(model) 410 | shared.opts.save(shared.config_filename) 411 | else: log('Could not find model and no URL was provided') 412 | 413 | def select_vae(resource: ResourceRequest): 414 | # TODO: find by hash instead of name 415 | if not resource['name'].endswith('.pt'): resource['name'] += '.pt' 416 | full_path = os.path.join(models_path, 'VAE', resource['name']) 417 | 418 | if sd_vae.loaded_vae_file is not None and sd_vae.get_filename(sd_vae.loaded_vae_file) == resource['name']: 419 | log('VAE already loaded') 420 | return 421 | 422 | isAvailable = load_if_missing(full_path, resource['url']) 423 | if not isAvailable: 424 | log('Could not find VAE') 425 | return 426 | 427 | sd_vae.refresh_vae_list() 428 | sd_vae.load_vae(shared.sd_model, full_path) 429 | 430 | def clear_vae(): 431 | log('Clearing VAE') 432 | sd_vae.clear_loaded_vae() 433 | 434 | def select_hypernetwork(resource: ResourceRequest): 435 | # TODO: find by hash instead of name 436 | if shared.opts.sd_hypernetwork == resource['name']: 437 | log('Hypernetwork already loaded') 438 | return 439 | 440 | full_path = os.path.join(shared.cmd_opts.hypernetwork_dir, resource['name']); 441 | if not full_path.endswith('.pt'): full_path += '.pt' 442 | isAvailable = load_if_missing(full_path, resource['url']) 443 | if not isAvailable: 444 | log('Could not find hypernetwork') 445 | return 446 | 447 | shared.opts.sd_hypernetwork = resource['name'] 448 | shared.opts.save(shared.config_filename) 449 | shared.reload_hypernetworks() 450 | 451 | def clear_hypernetwork(): 452 | if (shared.opts.sd_hypernetwork == 'None'): return 453 | 454 | log('Clearing hypernetwork') 455 | shared.opts.sd_hypernetwork = 'None' 456 | shared.opts.save(shared.config_filename) 457 | shared.reload_hypernetworks() 458 | #endregion 459 | 460 | #region Resource Management 461 | def update_resource_preview(hash: str, preview_url: str): 462 | resources = load_resource_list([]) 463 | matches = [resource for resource in resources if hash.lower() == resource['hash']] 464 | if len(matches) == 0: return 465 | 466 | for resource in matches: 467 | # download image and save to resource['path'] - ext + '.preview.png' 468 | preview_path = os.path.splitext(resource['path'])[0] + '.preview.png' 469 | download_file(preview_url, preview_path) 470 | 471 | #endregion Selecting Resources 472 | 473 | #region Activities 474 | activities = [] 475 | activity_history_length = 10 476 | ignore_activity_types = ['resources:list','activities:list','activities:clear', 'activities:cancel'] 477 | def add_activity(activity: Command): 478 | global activities 479 | 480 | if activity['type'] in ignore_activity_types: return 481 | 482 | existing_activity_index = [i for i, x in enumerate(activities) if x['id'] == activity['id']] 483 | if len(existing_activity_index) > 0: activities[existing_activity_index[0]] = activity 484 | else: activities.insert(0, activity) 485 | 486 | if len(activities) > activity_history_length: 487 | activities.pop() 488 | #endregion 489 | -------------------------------------------------------------------------------- /civitai/link.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | import time 3 | from typing import List 4 | import socketio 5 | 6 | import civitai.lib as civitai 7 | import civitai.generation as generation 8 | from civitai.models import Command, CommandActivitiesList, CommandImageTxt2Img, CommandResourcesAdd, CommandActivitiesCancel, CommandResourcesList, CommandResourcesRemove, ErrorPayload, JoinedPayload, RoomPresence, UpgradeKeyPayload 9 | 10 | from modules import shared, sd_models, script_callbacks, hashes 11 | 12 | #region Civitai Link Utils 13 | def log(message: str): 14 | if not shared.opts.data.get('civitai_link_logging', True): return 15 | print(f'Civitai Link: {message}') 16 | #endregion 17 | 18 | #region Civitai Link Command Handlers 19 | def send_resources(types: List[str] = []): 20 | command_response({'type': 'resources:list', 'resources': civitai.load_resource_list(types)}) 21 | 22 | def on_resources_list(payload: CommandResourcesList): 23 | types = payload['types'] if 'types' in payload else [] 24 | payload['resources'] = civitai.load_resource_list(types) 25 | payload['status'] = 'success' 26 | command_response(payload) 27 | 28 | def on_activities_list(payload: CommandActivitiesList): 29 | payload['activities'] = civitai.activities 30 | payload['status'] = 'success' 31 | command_response(payload) 32 | 33 | def on_activities_clear(payload: CommandActivitiesList): 34 | civitai.activities = [] 35 | payload['activities'] = civitai.activities 36 | payload['status'] = 'success' 37 | command_response(payload) 38 | 39 | report_interval = 1 40 | processing_activities: List[str] = [] 41 | should_cancel_activity: List[str] = [] 42 | def on_resources_add(payload: CommandResourcesAdd): 43 | resource = payload['resource'] 44 | payload['status'] = 'processing' 45 | 46 | last_report = time.time() 47 | def report_status(force=False): 48 | nonlocal last_report 49 | current_time = time.time() 50 | if force or current_time - last_report > report_interval: 51 | command_response(payload, history=True) 52 | last_report = current_time 53 | 54 | notified_of_download = False 55 | def on_progress(current: int, total: int, start_time: float): 56 | nonlocal notified_of_download 57 | if not notified_of_download: 58 | send_resources() 59 | notified_of_download = True 60 | 61 | if payload['id'] in should_cancel_activity: 62 | should_cancel_activity.remove(payload['id']) 63 | dl_resources = [r for r in civitai.resources if r['hash'] == resource['hash'] and r['downloading'] == True] 64 | if len(dl_resources) > 0: 65 | civitai.resources.remove(dl_resources[0]) 66 | payload['status'] = 'canceled' 67 | return True 68 | 69 | current_time = time.time() 70 | elapsed_time = current_time - start_time 71 | speed = current / elapsed_time 72 | remaining_time = (total - current) / speed 73 | progress = current / total * 100 74 | payload['status'] = 'processing' 75 | payload['progress'] = progress 76 | payload['remainingTime'] = remaining_time 77 | payload['speed'] = speed 78 | report_status() 79 | 80 | try: 81 | processing_activities.append(payload['id']) 82 | civitai.load_resource(resource, on_progress) 83 | if payload['status'] != 'canceled': 84 | payload['status'] = 'success' 85 | except Exception as e: 86 | log(e) 87 | if payload['status'] != 'canceled': 88 | payload['status'] = 'error' 89 | payload['error'] = 'Failed to download resource' 90 | 91 | processing_activities.remove(payload['id']) 92 | report_status(True) 93 | send_resources() 94 | 95 | def on_activities_cancel(payload: CommandActivitiesCancel): 96 | activity_id = payload['activityId'] 97 | if activity_id not in processing_activities: 98 | payload['status'] = 'error' 99 | payload['error'] = 'Activity not found or already completed' 100 | else: 101 | should_cancel_activity.append(activity_id) 102 | payload['status'] = 'success' 103 | 104 | command_response(payload) 105 | 106 | def on_resources_remove(payload: CommandResourcesRemove): 107 | try: 108 | civitai.remove_resource(payload['resource']) 109 | payload['status'] = 'success' 110 | except Exception as e: 111 | log(e) 112 | payload['status'] = 'error' 113 | payload['error'] = 'Failed to remove resource' 114 | 115 | command_response(payload, history=True) 116 | send_resources() 117 | 118 | def on_image_txt2img(payload: CommandImageTxt2Img): 119 | try: 120 | result = generation.txt2img(payload) 121 | payload['status'] = 'success' 122 | payload['images'] = result.images 123 | except Exception as e: 124 | log(e) 125 | payload['status'] = 'error' 126 | payload['error'] = 'Failed to generate image: ' + e 127 | 128 | command_response(payload) 129 | #endregion 130 | 131 | #region SocketIO Events 132 | try: 133 | socketio_url = shared.cmd_opts.civitai_link_endpoint 134 | except: 135 | socketio_url = 'https://link.civitai.com' 136 | 137 | sio = socketio.Client() 138 | should_reconnect = False 139 | 140 | @sio.event 141 | def connect(): 142 | global should_reconnect 143 | 144 | log('Connected to Civitai Link Server') 145 | sio.emit('iam', {"type": "sd"}) 146 | if should_reconnect: 147 | key = shared.opts.data.get("civitai_link_key", None) 148 | if key is None: return 149 | join_room(key) 150 | should_reconnect = False 151 | 152 | @sio.event 153 | def connect_error(data): 154 | log('Error connecting to Civitai Link Server') 155 | 156 | @sio.event 157 | def disconnect(): 158 | global should_reconnect 159 | global current_key 160 | current_key = None 161 | 162 | log('Disconnected from Civitai Link Server') 163 | should_reconnect = True 164 | 165 | @sio.on('command') 166 | def on_command(payload: Command): 167 | command = payload['type'] 168 | log(f"Incoming Command: {payload['type']}") 169 | civitai.add_activity(payload) 170 | 171 | if command == 'activities:list': return on_activities_list(payload) 172 | elif command == 'activities:clear': return on_activities_clear(payload) 173 | elif command == 'activities:cancel': return on_activities_cancel(payload) 174 | elif command == 'resources:list': return on_resources_list(payload) 175 | elif command == 'resources:add': return on_resources_add(payload) 176 | elif command == 'resources:remove': return on_resources_remove(payload) 177 | elif command == 'image:txt2img': return on_image_txt2img(payload) 178 | 179 | @sio.on('kicked') 180 | def on_kicked(): 181 | log(f"Kicked from instance. Clearing key.") 182 | shared.opts.data['civitai_link_key'] = None 183 | 184 | @sio.on('roomPresence') 185 | def on_room_presence(payload: RoomPresence): 186 | log(f"Presence update: SD: {payload['sd']}, Clients: {payload['client']}") 187 | connected = payload['sd'] > 0 and payload['client'] > 0 188 | if civitai.connected != connected: 189 | civitai.connected = connected 190 | if connected: log("Connected to Civitai Instance") 191 | else: log("Disconnected from Civitai Instance") 192 | 193 | upgraded_key = None 194 | @sio.on('upgradeKey') 195 | def on_upgrade_key(payload: UpgradeKeyPayload): 196 | global upgraded_key 197 | 198 | log("Link Key upgraded") 199 | upgraded_key = payload['key'] 200 | 201 | shared.opts.set('civitai_link_key', upgraded_key) 202 | shared.opts.save(shared.config_filename) 203 | 204 | @sio.on('error') 205 | def on_error(payload: ErrorPayload): 206 | log(f"Error: {payload['msg']}") 207 | 208 | def command_response(payload, history=False): 209 | payload['updatedAt'] = datetime.now(timezone.utc).isoformat() 210 | if history: civitai.add_activity(payload) 211 | sio.emit('commandStatus', payload) 212 | #endregion 213 | 214 | #region SocketIO Connection Management 215 | def socketio_connect(): 216 | if (sio.connected): return 217 | sio.connect(socketio_url, socketio_path='/api/socketio') 218 | def is_connected() -> bool: 219 | return sio.connected 220 | 221 | current_key = None 222 | def join_room(key): 223 | if (current_key == key): return 224 | def on_join(payload): 225 | log(f"Joined room {key}") 226 | sio.emit('join', key, callback=on_join) 227 | 228 | def rejoin_room(key): 229 | current_key = None 230 | join_room(key) 231 | 232 | old_short_key = None 233 | def on_civitai_link_key_changed(): 234 | global old_short_key 235 | 236 | if not sio.connected: socketio_connect() 237 | 238 | # If the key is upgraded, don't change it back to the short key 239 | if old_short_key is not None and old_short_key == shared.opts.data.get("civitai_link_key", None): 240 | shared.opts.data['civitai_link_key'] = upgraded_key 241 | return 242 | 243 | key = shared.opts.data.get("civitai_link_key", None) 244 | if len(key) < 10: old_short_key = key 245 | join_room(key) 246 | #endregion -------------------------------------------------------------------------------- /civitai/models.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import List 3 | from pydantic import BaseModel, Field 4 | 5 | class ResourceTypes(str, Enum): 6 | Checkpoint = "Checkpoint" 7 | CheckpointConfig = "CheckpointConfig" 8 | TextualInversion = "TextualInversion" 9 | AestheticGradient = "AestheticGradient" 10 | Hypernetwork = "Hypernetwork" 11 | LORA = "LORA" 12 | VAE = "VAE" 13 | 14 | class CommandTypes(str, Enum): 15 | ActivitiesList = "activities:list" 16 | ActivitiesCancel = "activities:cancel" 17 | ResourcesList = "resources:list" 18 | ResourcesAdd = "resources:add" 19 | ResourcesRemove = "resources:remove" 20 | 21 | class ImageParams(BaseModel): 22 | prompt: str = Field(default="", title="Prompt", description="The prompt to use when generating the image.") 23 | negativePrompt: str = Field(default="", title="Negative Prompt", description="The negative prompt to use when generating the image.") 24 | sampler: str = Field(default="Euler a", title="Sampler", description="The sampler to use when generating the image.") 25 | seed: int = Field(default=-1, title="Seed", description="The seed to use when generating the image.") 26 | steps: int = Field(default=30, title="Steps", description="The number of steps to use when generating the image.") 27 | width: int = Field(default=512, title="Width", description="The width of the image to generate.") 28 | height: int = Field(default=512, title="Height", description="The height of the image to generate.") 29 | cfgScale: float = Field(default=7.5, title="Scale", description="The guidance scale for image generation.") 30 | 31 | 32 | class GenerateImageRequest(BaseModel): 33 | quantity: int = Field(default=1, title="Quantity", description="The number of images to generate.") 34 | batchSize: int = Field(default=1, title="Batch Size", description="The number of images to generate in each batch.") 35 | model: str = Field(default=None, title="Model", description="The hash of the model to use when generating the images.") 36 | vae: str = Field(default=None, title="VAE", description="The hash of the VAE to use when generating the images.") 37 | params: ImageParams = Field(default=ImageParams(), title="Parameters", description="The parameters to use when generating the images.") 38 | 39 | class ResourceRequest(BaseModel): 40 | name: str = Field(default=None, title="Name", description="The name of the resource to download.") 41 | type: ResourceTypes = Field(default=None, title="Type", description="The type of the resource to download.") 42 | hash: str = Field(default=None, title="Hash", description="The SHA256 hash of the resource to download.") 43 | url: str = Field(default=None, title="URL", description="The URL of the resource to download.", required=False) 44 | previewImage: str = Field(default=None, title="Preview Image", description="The URL of the preview image.", required=False) 45 | 46 | class RoomPresence(BaseModel): 47 | client: int = Field(default=None, title="Clients", description="The number of clients in the room") 48 | sd: int = Field(default=None, title="Stable Diffusion Clients", description="The number of Stable Diffusion Clients in the room") 49 | 50 | class Command(BaseModel): 51 | id: str = Field(default=None, title="ID", description="The ID of the command.") 52 | type: CommandTypes = Field(default=None, title="Type", description="The type of command to execute.") 53 | 54 | class CommandActivitiesList(Command): 55 | type: CommandTypes = Field(default=CommandTypes.ActivitiesList, title="Type", description="The type of command to execute.") 56 | 57 | class CommandResourcesList(Command): 58 | type: CommandTypes = Field(default=CommandTypes.ResourcesList, title="Type", description="The type of command to execute.") 59 | types: List[ResourceTypes] = Field(default=[], title="Types", description="The types of resources to list.") 60 | 61 | class CommandResourcesAdd(Command): 62 | type: CommandTypes = Field(default=CommandTypes.ResourcesAdd, title="Type", description="The type of command to execute.") 63 | resource: ResourceRequest = Field(default=[], title="Resource", description="The resources to add.") 64 | 65 | class ResourceCancelRequest(BaseModel): 66 | type: ResourceTypes = Field(default=None, title="Type", description="The type of the resource to remove.") 67 | hash: str = Field(default=None, title="Hash", description="The SHA256 hash of the resource to remove.") 68 | 69 | class CommandActivitiesCancel(Command): 70 | type: CommandTypes = Field(default=CommandTypes.ActivitiesCancel, title="Type", description="The type of command to execute.") 71 | 72 | class ResourceRemoveRequest(BaseModel): 73 | type: ResourceTypes = Field(default=None, title="Type", description="The type of the resource to remove.") 74 | hash: str = Field(default=None, title="Hash", description="The SHA256 hash of the resource to remove.") 75 | 76 | class CommandResourcesRemove(Command): 77 | type: CommandTypes = Field(default=CommandTypes.ResourcesRemove, title="Type", description="The type of command to execute.") 78 | resource: ResourceRemoveRequest = Field(default=[], title="Resource", description="The resources to remove.") 79 | 80 | class CommandImageTxt2Img(Command): 81 | quantity: int = Field(default=1, title="Quantity", description="The number of images to generate.") 82 | batchSize: int = Field(default=1, title="Batch Size", description="The number of images to generate in each batch.") 83 | model: str = Field(default=None, title="Model", description="The hash of the model to use when generating the images.") 84 | vae: str = Field(default=None, title="VAE", description="The hash of the VAE to use when generating the images.") 85 | params: ImageParams = Field(default=ImageParams(), title="Parameters", description="The parameters to use when generating the images.") 86 | 87 | class UpgradeKeyPayload(BaseModel): 88 | key: str = Field(default=None, title="Key", description="The upgraded key.") 89 | 90 | class ErrorPayload(BaseModel): 91 | msg: str = Field(default=None, title="Message", description="The error message.") 92 | 93 | class JoinedPayload(BaseModel): 94 | type: str = Field(default=None, title="Type", description="The type of the client that joined.") -------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | import sys 4 | 5 | import git 6 | 7 | from launch import run 8 | 9 | req_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "requirements.txt") 10 | 11 | def is_package_installed(package_name, version): 12 | # strip [] from package name 13 | package_name = package_name.split("[")[0] 14 | try: 15 | result = subprocess.run(['pip', 'show', package_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 16 | except FileNotFoundError: 17 | return False 18 | if result.returncode == 0: 19 | for line in result.stdout.decode('utf-8').splitlines(): 20 | if line.startswith('Version: '): 21 | installed_version = line.split(' ')[-1] 22 | if installed_version == version: 23 | return True 24 | return False 25 | 26 | def check_versions(): 27 | global req_file 28 | reqs = open(req_file, 'r') 29 | lines = reqs.readlines() 30 | reqs_dict = {} 31 | for line in lines: 32 | splits = line.split("==") 33 | if len(splits) == 2: 34 | key = splits[0] 35 | reqs_dict[key] = splits[1].replace("\n", "").strip() 36 | # Loop through reqs and check if installed 37 | for req in reqs_dict: 38 | available = is_package_installed(req, reqs_dict[req]) 39 | if available: print(f"[+] {req} version {reqs_dict[req]} installed.") 40 | else : print(f"[!] {req} version {reqs_dict[req]} NOT installed.") 41 | 42 | base_dir = os.path.dirname(os.path.realpath(__file__)) 43 | revision = "" 44 | app_revision = "" 45 | 46 | try: 47 | repo = git.Repo(base_dir) 48 | revision = repo.rev_parse("HEAD") 49 | app_repo = git.Repo(os.path.join(base_dir, "..", "..")) 50 | app_revision = app_repo.rev_parse("HEAD") 51 | except: 52 | pass 53 | 54 | print("") 55 | print("#######################################################################################################") 56 | print("Initializing Civitai Link") 57 | print("If submitting an issue on github, please provide the below text for debugging purposes:") 58 | print("") 59 | print(f"Python revision: {sys.version}") 60 | print(f"Civitai Link revision: {revision}") 61 | print(f"SD-WebUI revision: {app_revision}") 62 | print("") 63 | civitai_skip_install = os.environ.get('CIVITAI_SKIP_INSTALL', False) 64 | 65 | try: 66 | requirements_file = os.environ.get('REQS_FILE', "requirements_versions.txt") 67 | if requirements_file == req_file: 68 | civitai_skip_install = True 69 | except: 70 | pass 71 | 72 | if not civitai_skip_install: 73 | name = "Civitai Link" 74 | run(f'"{sys.executable}" -m pip install -r "{req_file}"', f"Checking {name} requirements...", 75 | f"Couldn't install {name} requirements.") 76 | 77 | check_versions() 78 | print("") 79 | print("#######################################################################################################") 80 | -------------------------------------------------------------------------------- /javascript/across-tabs.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * across-tabs "1.2.4" 4 | * https://github.com/wingify/across-tabs 5 | * MIT licensed 6 | * 7 | * Copyright (C) 2017-2019 Wingify Pvt. Ltd. - Authored by Varun Malhotra(https://github.com/softvar) 8 | * 9 | */ 10 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("AcrossTabs",[],t):"object"==typeof exports?exports.AcrossTabs=t():e.AcrossTabs=t()}(this,function(){return function(e){function t(i){if(n[i])return n[i].exports;var a=n[i]={i:i,l:!1,exports:{}};return e[i].call(a.exports,a,a.exports,t),a.l=!0,a.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,i){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:i})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=6)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i={LOADED:"__TAB__LOADED_EVENT__",CUSTOM:"__TAB__CUSTOM_EVENT__",HANDSHAKE:"__TAB__HANDSHAKE_EVENT__",ON_BEFORE_UNLOAD:"__TAB__ON_BEFORE_UNLOAD__",PARENT_DISCONNECTED:"__PARENT_DISCONNECTED__",HANDSHAKE_WITH_PARENT:"__HANDSHAKE_WITH_PARENT__",PARENT_COMMUNICATED:"__PARENT_COMMUNICATED__"};t.default=i},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i={INVALID_JSON:"Invalid JSON Object!",INVALID_DATA:"Some wrong message is being sent by Parent.",CONFIG_REQUIRED:"Configuration options required. Please read docs.",CUSTOM_EVENT:"CustomEvent(and it's polyfill) is not supported in this browser.",URL_REQUIRED:"Url is needed for creating and opening a new window/tab. Please read docs."};t.default=i},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(0),o=i(a),r=n(3),s=i(r),u=n(4),d=i(u),l=n(1),f=i(l),c={tabs:[],config:{}};c._remove=function(e){var t=void 0;t=s.default.searchByKeyName(c.tabs,"id",e.id,"index"),c.tabs.splice(t,1)},c._preProcessMessage=function(e){try{e=c.config.stringify(e)}catch(e){throw new Error(f.default.INVALID_JSON)}return e&&-1===e.indexOf(o.default.PARENT_COMMUNICATED)&&(e=o.default.PARENT_COMMUNICATED+e),e},c.addNew=function(e){c.tabs.push(e)},c.getOpened=function(){return c.tabs.filter(function(e){return e.status===d.default.OPEN})},c.getClosed=function(){return c.tabs.filter(function(e){return e.status===d.default.CLOSE})},c.getAll=function(){return c.tabs},c.closeTab=function(e){var t=s.default.searchByKeyName(c.tabs,"id",e);return t&&t.ref&&(t.ref.close(),t.status=d.default.CLOSE),c},c.closeAll=function(){var e=void 0;for(e=0;e0;a>>>=1,o+=o)1&a&&(i=o+i);return i}},e._hexAligner=e._getIntAligner(16),e.prototype.toString=function(){return this.hexString},e}(),t.default=i},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(3),o=i(a),r=n(2),s=i(r),u=n(1),d=i(u),l=n(0),f=i(l);n(11);var c={};c._onLoad=function(e){var t=void 0,n=void 0,i=e.split(f.default.LOADED)[1];if(i)try{i=s.default.config.parse(i),i.id&&(t=s.default.getAll(),t.length&&(window.newlyTabOpened=t[t.length-1],window.newlyTabOpened.id=i.id,window.newlyTabOpened.name=i.name||i.windowName))}catch(e){throw new Error(d.default.INVALID_JSON)}if(window.newlyTabOpened)try{n=f.default.HANDSHAKE_WITH_PARENT,n+=s.default.config.stringify({id:window.newlyTabOpened.id,name:window.newlyTabOpened.name||window.newlyTabOpened.windowName,parentName:window.name}),s.default.sendMessage(window.newlyTabOpened,n,i.isSiteInsideFrame)}catch(e){throw new Error(d.default.INVALID_JSON)}},c._onCustomMessage=function(e,t){var n=void 0,i=void 0,a=e.split(t)[1];try{a=s.default.config.parse(a)}catch(e){throw new Error(d.default.INVALID_JSON)}i={tabInfo:a,type:t},n=new CustomEvent("onCustomChildMessage",{detail:i}),window.dispatchEvent(n)},c._onBeforeUnload=function(e){var t=void 0,n=e.split(f.default.ON_BEFORE_UNLOAD)[1];try{n=s.default.config.parse(n)}catch(e){throw new Error(d.default.INVALID_JSON)}s.default.tabs.length&&(t=s.default.getAll(),window.newlyTabOpened=o.default.searchByKeyName(t,"id",n.id)||window.newlyTabOpened);var i=new CustomEvent("onChildUnload",{detail:n});window.dispatchEvent(i)},c.onNewTab=function(e){var t=e.data;return!(!t||"string"!=typeof t||!s.default.tabs.length)&&((!s.default.config.origin||s.default.config.origin===e.origin)&&void(t.indexOf(f.default.LOADED)>-1?c._onLoad(t):t.indexOf(f.default.CUSTOM)>-1?c._onCustomMessage(t,f.default.CUSTOM):t.indexOf(f.default.HANDSHAKE)>-1?c._onCustomMessage(t,f.default.HANDSHAKE):t.indexOf(f.default.ON_BEFORE_UNLOAD)>-1&&c._onBeforeUnload(t)))},t.default=c},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(){function e(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=document.createEvent("CustomEvent");return n.initCustomEvent(e,!1,!1,t.detail),n}"function"!=typeof window.CustomEvent&&(e.prototype=window.Event.prototype,window.CustomEvent=e)}()},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t-1&&(this.config.onParentDisconnect&&this.config.onParentDisconnect(),window.removeEventListener("message",function(e){return t.onCommunication(e)})),i.indexOf(u.default.HANDSHAKE_WITH_PARENT)>-1){var a=void 0;n=i.split(u.default.HANDSHAKE_WITH_PARENT)[1],this._setData(n),this._parseData(n),a={id:this.tabId,isSiteInsideFrame:this.config.isSiteInsideFrame},this.sendMessageToParent(a,u.default.HANDSHAKE),this.config.onInitialize&&this.config.onInitialize()}if(i.indexOf(u.default.PARENT_COMMUNICATED)>-1){n=i.split(u.default.PARENT_COMMUNICATED)[1];try{n=this.config.parse(n)}catch(e){throw new Error(l.default.INVALID_JSON)}this.config.onParentCommunication&&this.config.onParentCommunication(n)}}}},{key:"addListeners",value:function(){var e=this;window.onbeforeunload=function(t){var n={id:e.tabId,isSiteInsideFrame:e.config.isSiteInsideFrame};e.sendMessageToParent(n,u.default.ON_BEFORE_UNLOAD)},window.removeEventListener("message",function(t){return e.onCommunication(t)}),window.addEventListener("message",function(t){return e.onCommunication(t)})}},{key:"setHandshakeExpiry",value:function(){var e=this;return window.setTimeout(function(){e.config.onHandShakeExpiry&&e.config.onHandShakeExpiry()},this.handshakeExpiryLimit)}},{key:"sendMessageToParent",value:function(e,t){var n=void 0;e=(t||u.default.CUSTOM)+this.config.stringify(e),window.top.opener&&(n=this.config.origin||"*",window.top.opener.postMessage(e,n))}},{key:"getTabInfo",value:function(){return{id:this.tabId,name:this.tabName,parentName:this.tabParentName,isSiteInsideFrame:this.config.isSiteInsideFrame}}},{key:"init",value:function(){this.isSessionStorageSupported=this._isSessionStorage(),this.addListeners(),this._restoreData(),this.sendMessageToParent(this.getTabInfo(),u.default.LOADED),this.timeout=this.setHandshakeExpiry(),this.config.onReady&&this.config.onReady()}}]),e}();t.default=f}])}); -------------------------------------------------------------------------------- /javascript/civitai.js: -------------------------------------------------------------------------------- 1 | (async function () { 2 | // #region [utils] 3 | const log = (...args) => console.log(`[civitai]`, ...args); 4 | const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); 5 | const getElement = (selector, timeout = 10000) => new Promise((resolve, reject) => { 6 | const interval = setInterval(() => { 7 | const el = gradioApp().querySelector(selector); 8 | timeout -= 100; 9 | if (timeout < 0) { 10 | reject('timeout'); 11 | clearInterval(interval); 12 | } else if (el) { 13 | resolve(el); 14 | clearInterval(interval); 15 | } 16 | }, 100); 17 | }) 18 | // #endregion 19 | 20 | 21 | async function generate() { 22 | const generateButton = await getElement('#txt2img_generate'); 23 | generateButton.click(); 24 | log('generating image'); 25 | } 26 | 27 | async function handlePrompt(prompt, andGenerate = false, delayMs = 3000) { 28 | log('injecting prompt', prompt); 29 | const promptEl = await getElement('#txt2img_prompt textarea'); 30 | promptEl.value = prompt; 31 | promptEl.dispatchEvent(new Event("input", { bubbles: true })); // internal Svelte trigger 32 | 33 | const pastePromptButton = await getElement('#paste'); 34 | pastePromptButton.click(); 35 | log('applying prompt'); 36 | 37 | if (andGenerate) { 38 | await delay(delayMs); 39 | await generate(); 40 | notifyParent({generate: true}); 41 | } 42 | } 43 | 44 | function notifyParent(msg) { 45 | if (child && child.sendMessageToParent) 46 | child.sendMessageToParent(msg); 47 | } 48 | 49 | async function refreshModels() { 50 | const refreshModelsButton = await getElement('#refresh_sd_model_checkpoint'); 51 | refreshModelsButton.click(); 52 | } 53 | 54 | let child; 55 | async function hookChild() { 56 | child = new AcrossTabs.default.Child({ 57 | // origin: 'https://civitai.com', 58 | origin: 'http://localhost:3000', 59 | onParentCommunication: commandHandler 60 | }); 61 | } 62 | 63 | function commandHandler({ command, ...data }) { 64 | log('tab communication', { command, data }) 65 | switch (command) { 66 | case 'generate': return handlePrompt(data.generationParams, true, 500); 67 | case 'refresh-models': return refreshModels(); 68 | } 69 | } 70 | 71 | let statusElement = document.createElement('div'); 72 | let alphaStatusElement = document.createElement('div'); 73 | let currentStatus = false; 74 | let currentAlphaStatus = false; 75 | async function checkStatus() { 76 | const { connected,alpha_connected } = await fetch('/civitai/v1/link-status').then(x=>x.json()); 77 | if (currentStatus != connected) { 78 | currentStatus = connected; 79 | statusElement.classList.toggle('connected', connected); 80 | } 81 | if (currentAlphaStatus != alpha_connected) { 82 | currentAlphaStatus = alpha_connected; 83 | alphaStatusElement.classList.toggle('connected', alpha_connected); 84 | } 85 | } 86 | async function checkAlphaStatus() { 87 | const { connected } = await fetch('/civitai/v1/alpha-link-status').then(x=>x.json()); 88 | if (currentStatus != connected) { 89 | currentStatus = connected; 90 | alphaStatusElement.classList.toggle('alpha-connected', connected); 91 | } 92 | } 93 | async function startStatusChecks() { 94 | statusElement.id = 'civitai-status'; 95 | 96 | statusElement.classList.add('civitai-status'); 97 | alphaStatusElement.id = 'civitai-alpha-status'; 98 | alphaStatusElement.classList.add('civitai-alpha-status'); 99 | await getElement('.gradio-container'); // wait for gradio to load 100 | gradioApp().appendChild(statusElement); 101 | gradioApp().appendChild(alphaStatusElement); 102 | alphaStatusElement.addEventListener('click',async function(){ 103 | if(true || !currentAlphaStatus) 104 | { 105 | const { message } = await fetch('/civitai/v1/reconnect-link').then(x=>x.json()); 106 | if(message == 'Civitai Link active') 107 | { 108 | notify({success:true,message:message}); 109 | } 110 | else 111 | { 112 | notify({success:false,message:message}); 113 | } 114 | console.log(message); 115 | } 116 | else 117 | { 118 | notify({success:true,message:'Not disconnected'}); 119 | } 120 | 121 | }); 122 | setInterval(checkStatus, 1000 * 10); 123 | checkStatus(); 124 | } 125 | 126 | 127 | // Bootstrap 128 | const searchParams = new URLSearchParams(location.search); 129 | if (searchParams.has('civitai_prompt')) 130 | handlePrompt(atob(searchParams.get('civitai_prompt')), searchParams.has('civitai_generate')); 131 | if (searchParams.has('civitai_refresh_models')) 132 | refreshModels() 133 | if (searchParams.has('civitai_hook_child')) 134 | hookChild(); 135 | 136 | // clear search params 137 | history.replaceState({}, document.title, location.href); 138 | await startStatusChecks(); 139 | })(); 140 | -------------------------------------------------------------------------------- /preload.py: -------------------------------------------------------------------------------- 1 | # loaded before parsing commandline args 2 | import argparse 3 | 4 | def preload(parser: argparse.ArgumentParser): 5 | parser.add_argument("--civitai-endpoint", type=str, help="Endpoint for interacting with a Civitai instance", default="https://civitai.com/api/v1") 6 | parser.add_argument("--civitai-link-endpoint", type=str, help="Endpoint for interacting with a Civitai Link server", default="https://link.civitai.com/api/socketio") -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-socketio[client]>=5.7.2 2 | -------------------------------------------------------------------------------- /scripts/api.py: -------------------------------------------------------------------------------- 1 | # api endpoints 2 | import gradio as gr 3 | from fastapi import FastAPI 4 | from scripts.link import reconnect_to_civitai,get_link_status 5 | from modules import script_callbacks as script_callbacks 6 | 7 | import civitai.lib as civitai 8 | from civitai.models import GenerateImageRequest, ResourceRequest 9 | 10 | def civitaiAPI(_: gr.Blocks, app: FastAPI): 11 | @app.get('/civitai/v1/link-status') 12 | def link_status(): 13 | return { "connected": civitai.connected } 14 | @app.get('/civitai/v1/reconnect-link') 15 | def reconnect_link(): 16 | msg = reconnect_to_civitai() 17 | return { "message": msg } 18 | @app.get('/civitai/v1/alpha-link-status') 19 | def alpha_link_status(): 20 | return { "connected": get_link_status() } 21 | script_callbacks.on_app_started(civitaiAPI) 22 | civitai.log("API loaded") 23 | -------------------------------------------------------------------------------- /scripts/gen_hashing.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | import os 4 | 5 | import civitai.lib as civitai 6 | from modules import script_callbacks, sd_vae, shared 7 | 8 | additional_network_type_map = { 9 | 'lora': 'LORA', 10 | 'hypernet': 'Hypernetwork' 11 | } 12 | additional_network_pattern = r'<(lora|hypernet):([a-zA-Z0-9_\.\-\s]+):([0-9.]+)(?:[:].*)?>' 13 | model_hash_pattern = r'Model hash: ([0-9a-fA-F]{10})' 14 | 15 | # Automatically pull model with corresponding hash from Civitai 16 | def add_resource_hashes(params): 17 | if 'parameters' not in params.pnginfo: return 18 | 19 | hashify_resources = shared.opts.data.get('civitai_hashify_resources', True) 20 | if not hashify_resources: return 21 | 22 | lines = params.pnginfo['parameters'].split('\n') 23 | generation_params = lines.pop() 24 | prompt_parts = '\n'.join(lines).split('Negative prompt:') 25 | prompt, negative_prompt = [s.strip() for s in prompt_parts[:2] + ['']*(2-len(prompt_parts))] 26 | 27 | hashed_prompt = prompt 28 | hashed_negative_prompt = negative_prompt 29 | 30 | resources = civitai.load_resource_list([]) 31 | resource_hashes = {} 32 | 33 | # Hash the VAE 34 | if hashify_resources and sd_vae.loaded_vae_file is not None: 35 | vae_name = os.path.splitext(sd_vae.get_filename(sd_vae.loaded_vae_file))[0] 36 | vae_matches = [r for r in resources if r['type'] == 'VAE' and r['name'] == vae_name] 37 | if len(vae_matches) > 0: 38 | short_hash = vae_matches[0]['hash'][:10] 39 | resource_hashes['vae'] = short_hash 40 | 41 | 42 | # Check for embeddings in prompt 43 | embeddings = [r for r in resources if r['type'] == 'TextualInversion'] 44 | for embedding in embeddings: 45 | embedding_name = embedding['name'] 46 | embedding_pattern = re.compile(r'(? 0: 62 | short_hash = matching_resource[0]['hash'][:10] 63 | resource_hashes[f'{network_type}:{network_name}'] = short_hash 64 | 65 | # Check for model hash in generation parameters 66 | model_match = re.search(model_hash_pattern, generation_params) 67 | if hashify_resources and model_match: 68 | model_hash = model_match.group(1) 69 | matching_resource = [r for r in resources if r['type'] == 'Checkpoint' and r['hash'].startswith(model_hash)] 70 | if len(matching_resource) > 0: 71 | short_hash = matching_resource[0]['hash'][:10] 72 | resource_hashes['model'] = short_hash 73 | 74 | if len(resource_hashes) > 0: 75 | params.pnginfo['parameters'] += f", Hashes: {json.dumps(resource_hashes)}" 76 | 77 | script_callbacks.on_before_image_saved(add_resource_hashes) 78 | -------------------------------------------------------------------------------- /scripts/info.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | import gradio as gr 3 | import threading 4 | import json 5 | from pathlib import Path 6 | 7 | import civitai.lib as civitai 8 | 9 | from modules import script_callbacks, shared 10 | 11 | actionable_types = ['LORA', 'LoCon', 'Hypernetwork', 'TextualInversion', 'Checkpoint'] 12 | 13 | 14 | def load_info(): 15 | download_missing_previews = shared.opts.data.get('civitai_download_triggers', True) 16 | if not download_missing_previews: 17 | return 18 | 19 | civitai.log("Check resources for missing info files") 20 | resources = civitai.load_resource_list() 21 | resources = [r for r in resources if r['type'] in actionable_types] 22 | 23 | # get all resources that have no info files 24 | missing_info = [r for r in resources if r['hasInfo'] is False] 25 | civitai.log(f"Found {len(missing_info)} resources missing info files") 26 | hashes = [r['hash'] for r in missing_info] 27 | 28 | # split hashes into batches of 100 and fetch into results 29 | results = [] 30 | try: 31 | for i in range(0, len(hashes), 100): 32 | batch = hashes[i:i + 100] 33 | results.extend(civitai.get_all_by_hash(batch)) 34 | except: 35 | civitai.log("Failed to fetch info from Civitai") 36 | return 37 | 38 | if len(results) == 0: 39 | civitai.log("No info found on Civitai") 40 | return 41 | 42 | civitai.log(f"Found {len(results)} hash matches") 43 | 44 | # update the resources with the new info 45 | updated = 0 46 | for r in results: 47 | if (r is None): 48 | continue 49 | 50 | for file in r['files']: 51 | if not 'hashes' in file or not 'SHA256' in file['hashes']: 52 | continue 53 | file_hash = file['hashes']['SHA256'] 54 | if file_hash.lower() not in hashes: 55 | continue 56 | 57 | if "SD 1" in r['baseModel']: 58 | sd_version = "SD1" 59 | elif "SD 2" in r['baseModel']: 60 | sd_version = "SD2" 61 | elif "SDXL" in r['baseModel']: 62 | sd_version = "SDXL" 63 | else: 64 | sd_version = "unknown" 65 | data = { 66 | "description": r['description'], 67 | "sd version": sd_version, 68 | "activation text": ", ".join(r['trainedWords']), 69 | "preferred weight": 0.8, 70 | "notes": "", 71 | } 72 | 73 | matches = [resource for resource in missing_info if file_hash.lower() == resource['hash']] 74 | if len(matches) == 0: 75 | continue 76 | 77 | for resource in matches: 78 | Path(resource['path']).with_suffix(".json").write_text(json.dumps(data, indent=4)) 79 | updated += 1 80 | 81 | civitai.log(f"Updated {updated} info files") 82 | 83 | 84 | # Automatically pull model with corresponding hash from Civitai 85 | def start_load_info(demo: gr.Blocks, app): 86 | civitai.refresh_info_function = load_info 87 | thread = threading.Thread(target=load_info) 88 | thread.start() 89 | 90 | 91 | script_callbacks.on_app_started(start_load_info) 92 | -------------------------------------------------------------------------------- /scripts/link.py: -------------------------------------------------------------------------------- 1 | import gradio as gr 2 | 3 | import civitai.link as link 4 | 5 | from modules import shared, script_callbacks 6 | 7 | def connect_to_civitai(demo: gr.Blocks, app): 8 | key = shared.opts.data.get("civitai_link_key", None) 9 | # If key is empty or not set, don't connect to Civitai Link 10 | if not key: return 11 | 12 | link.log('Connecting to Civitai Link Server') 13 | link.socketio_connect() 14 | link.join_room(key) 15 | def get_link_status()-> bool: 16 | return link.is_connected() 17 | 18 | def reconnect_to_civitai() -> str: 19 | key = shared.opts.data.get("civitai_link_key", None) 20 | # If key is empty or not set, don't connect to Civitai Link 21 | if not key: 22 | msg = 'Civitai Link Key is empty' 23 | link.log(msg) 24 | return msg 25 | 26 | link.log('Reconnecting to Civitai Link Server') 27 | link.socketio_connect() 28 | link.rejoin_room(key) 29 | if link.is_connected(): 30 | msg = 'Civitai Link active' 31 | link.log(msg) 32 | return msg 33 | else: 34 | return 'Civitai Link not connected' 35 | 36 | script_callbacks.on_app_started(connect_to_civitai) 37 | 38 | -------------------------------------------------------------------------------- /scripts/pasted.py: -------------------------------------------------------------------------------- 1 | import civitai.lib as civitai 2 | 3 | from modules import shared, script_callbacks 4 | 5 | # Automatically pull model with corresponding hash from Civitai 6 | def on_infotext_pasted(infotext, params): 7 | download_missing_models = shared.opts.data.get('civitai_download_missing_models', True) 8 | if (not download_missing_models or "Model hash" not in params or shared.opts.disable_weights_auto_swap): 9 | return 10 | 11 | model_hash = params["Model hash"] 12 | model = civitai.get_model_by_hash(model_hash) 13 | if (model is None): 14 | civitai.fetch_model_by_hash(model_hash) 15 | 16 | script_callbacks.on_infotext_pasted(on_infotext_pasted) 17 | -------------------------------------------------------------------------------- /scripts/previews.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | import gradio as gr 3 | import threading 4 | 5 | import civitai.lib as civitai 6 | 7 | from modules import script_callbacks, shared 8 | 9 | previewable_types = ['LORA', 'LoCon', 'Hypernetwork', 'TextualInversion', 'Checkpoint'] 10 | def load_previews(): 11 | download_missing_previews = shared.opts.data.get('civitai_download_previews', True) 12 | if not download_missing_previews: return 13 | nsfw_previews = shared.opts.data.get('civitai_nsfw_previews', True) 14 | 15 | civitai.log(f"Check resources for missing preview images") 16 | resources = civitai.load_resource_list() 17 | resources = [r for r in resources if r['type'] in previewable_types] 18 | 19 | # get all resources that are missing previews 20 | missing_previews = [r for r in resources if r['hasPreview'] is False] 21 | civitai.log(f"Found {len(missing_previews)} resources missing preview images") 22 | hashes = [r['hash'] for r in missing_previews] 23 | 24 | # split hashes into batches of 100 and fetch into results 25 | results = [] 26 | try: 27 | for i in range(0, len(hashes), 100): 28 | batch = hashes[i:i + 100] 29 | results.extend(civitai.get_all_by_hash(batch)) 30 | except: 31 | civitai.log("Failed to fetch preview images from Civitai") 32 | return 33 | 34 | if len(results) == 0: 35 | civitai.log("No preview images found on Civitai") 36 | return 37 | 38 | civitai.log(f"Found {len(results)} hash matches") 39 | 40 | # update the resources with the new preview 41 | updated = 0 42 | for r in results: 43 | if (r is None): continue 44 | 45 | for file in r['files']: 46 | if not 'hashes' in file or not 'SHA256' in file['hashes']: continue 47 | hash = file['hashes']['SHA256'] 48 | if hash.lower() not in hashes: continue 49 | images = r['images'] 50 | if (nsfw_previews is False): images = [i for i in images if i['nsfw'] is False] 51 | if (len(images) == 0): continue 52 | image_url = images[0]['url'] 53 | civitai.update_resource_preview(hash, image_url) 54 | updated += 1 55 | 56 | civitai.log(f"Updated {updated} preview images") 57 | 58 | # Automatically pull model with corresponding hash from Civitai 59 | def start_load_previews(demo: gr.Blocks, app): 60 | civitai.refresh_previews_function = load_previews 61 | thread = threading.Thread(target=load_previews) 62 | thread.start() 63 | 64 | script_callbacks.on_app_started(start_load_previews) 65 | -------------------------------------------------------------------------------- /scripts/settings.py: -------------------------------------------------------------------------------- 1 | from civitai.link import on_civitai_link_key_changed 2 | from modules import shared, script_callbacks 3 | 4 | def on_ui_settings(): 5 | section = ('civitai_link', "Civitai") 6 | shared.opts.add_option("civitai_link_key", shared.OptionInfo("", "Your Civitai Link Key", section=section, onchange=on_civitai_link_key_changed)) 7 | shared.opts.add_option("civitai_link_logging", shared.OptionInfo(True, "Show Civitai Link events in the console", section=section)) 8 | shared.opts.add_option("civitai_api_key", shared.OptionInfo("", "Your Civitai API Key", section=section)) 9 | shared.opts.add_option("civitai_download_previews", shared.OptionInfo(True, "Download missing preview images on startup", section=section)) 10 | shared.opts.add_option("civitai_download_triggers", shared.OptionInfo(True, "Download missing activation triggers on startup", section=section)) 11 | shared.opts.add_option("civitai_nsfw_previews", shared.OptionInfo(False, "Download NSFW (adult) preview images", section=section)) 12 | shared.opts.add_option("civitai_download_missing_models", shared.OptionInfo(True, "Download missing models upon reading generation parameters from prompt", section=section)) 13 | shared.opts.add_option("civitai_hashify_resources", shared.OptionInfo(True, "Include resource hashes in image metadata (for resource auto-detection on Civitai)", section=section)) 14 | shared.opts.add_option("civitai_folder_model", shared.OptionInfo("", "Models directory (if not default)", section=section)) 15 | shared.opts.add_option("civitai_folder_lora", shared.OptionInfo("", "LoRA directory (if not default)", section=section)) 16 | shared.opts.add_option("civitai_folder_lyco", shared.OptionInfo("", "LyCORIS directory (if not default)", section=section)) 17 | 18 | 19 | script_callbacks.on_ui_settings(on_ui_settings) -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | div.civitai-status{ 2 | position: absolute; 3 | top: 7px; 4 | right: 5px; 5 | width:24px; 6 | height:24px; 7 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 178 178' style='enable-background:new 0 0 178 178' xml:space='preserve'%3E%3ClinearGradient id='a' gradientUnits='userSpaceOnUse' x1='89.3' y1='1.5' x2='89.3' y2='177.014'%3E%3Cstop offset='0' style='stop-color:%23081692'/%3E%3Cstop offset='1' style='stop-color:%231e043c'/%3E%3C/linearGradient%3E%3ClinearGradient id='b' gradientUnits='userSpaceOnUse' x1='89.3' y1='1.5' x2='89.3' y2='177.014'%3E%3Cstop offset='0' style='stop-color:%231284f7'/%3E%3Cstop offset='1' style='stop-color:%230a20c9'/%3E%3C/linearGradient%3E%3Cpath style='fill:url(%23a)' d='M13.3 45.4v87.7l76 43.9 76-43.9V45.4l-76-43.9z'/%3E%3Cpath style='fill:url(%23b)' d='m89.3 29.2 52 30v60l-52 30-52-30v-60l52-30m0-27.7-76 43.9v87.8l76 43.9 76-43.9V45.4l-76-43.9z'/%3E%3Cpath style='fill:%23fff' d='m104.1 97.2-14.9 8.5-14.9-8.5v-17l14.9-8.5 14.9 8.5h18.2V69.7l-33-19-33 19v38.1l33 19 33-19V97.2z'/%3E%3C/svg%3E"); 8 | z-index: 1000; 9 | } 10 | 11 | div.civitai-status:before { 12 | width:6px; 13 | height:6px; 14 | background: red; 15 | border-radius: 50%; 16 | content: ''; 17 | position:absolute; 18 | top:-4px; 19 | right:1px; 20 | border: 1px solid rgba(255,255,255,0.3); 21 | } 22 | div.civitai-alpha-status{ 23 | position: absolute; 24 | top: 36px; 25 | right: 5px; 26 | width:24px; 27 | height:24px; 28 | background-repeat: no-repeat; 29 | background-size: cover; 30 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='1em' viewBox='0 0 512 512'%3E%3C!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --%3E%3Cstyle%3Esvg%7Bfill:%23ff0000%7D%3C/style%3E%3Cpath d='M228.3 469.1L47.6 300.4c-4.2-3.9-8.2-8.1-11.9-12.4h87c22.6 0 43-13.6 51.7-34.5l10.5-25.2 49.3 109.5c3.8 8.5 12.1 14 21.4 14.1s17.8-5 22-13.3L320 253.7l1.7 3.4c9.5 19 28.9 31 50.1 31H476.3c-3.7 4.3-7.7 8.5-11.9 12.4L283.7 469.1c-7.5 7-17.4 10.9-27.7 10.9s-20.2-3.9-27.7-10.9zM503.7 240h-132c-3 0-5.8-1.7-7.2-4.4l-23.2-46.3c-4.1-8.1-12.4-13.3-21.5-13.3s-17.4 5.1-21.5 13.3l-41.4 82.8L205.9 158.2c-3.9-8.7-12.7-14.3-22.2-14.1s-18.1 5.9-21.8 14.8l-31.8 76.3c-1.2 3-4.2 4.9-7.4 4.9H16c-2.6 0-5 .4-7.3 1.1C3 225.2 0 208.2 0 190.9v-5.8c0-69.9 50.5-129.5 119.4-141C165 36.5 211.4 51.4 244 84l12 12 12-12c32.6-32.6 79-47.5 124.6-39.9C461.5 55.6 512 115.2 512 185.1v5.8c0 16.9-2.8 33.5-8.3 49.1z'/%3E%3C/svg%3E"); 31 | z-index: 1000; 32 | } 33 | div.civitai-alpha-status:before { 34 | pointer-events:all; 35 | width:6px; 36 | height:6px; 37 | background: red; 38 | border-radius: 50%; 39 | content: ''; 40 | position:absolute; 41 | top:-4px; 42 | right:1px; 43 | border: 1px solid rgba(255,255,255,0.3); 44 | } 45 | /* blinking animation */ 46 | @keyframes blink { 47 | 0% { opacity: 0.2; } 48 | 50% { opacity: 1; } 49 | 100% { opacity: 0.2; } 50 | } 51 | 52 | div.civitai-status.connected:before { 53 | background:green; 54 | animation: blink 1s ease-in-out infinite; 55 | } 56 | div.civitai-alpha-status.connected:before { 57 | background:green; 58 | animation: blink 1s ease-in-out infinite; 59 | } --------------------------------------------------------------------------------