2024-10-27 03:15:28 +07:00
import os
import shutil
import datetime
import threading
import time
import json
from flask import jsonify
from utils . websocket_utils import send_websocket_message , active_websockets
from utils . app_configs import ( get_app_configs )
2024-10-30 20:22:42 +07:00
### shared_models-v0.9.1 by lutzapps, Oct 30th 2024 ###
### dev-my-v0.6
2024-10-27 03:15:28 +07:00
# to run (and optionally DEBUG) this docker image "better-ai-launcher" in a local container on your own machine
# you need to define the ENV var "LOCAL_DEBUG" in the "VSCode Docker Extension"
# file ".vscode/tasks.json" in the ENV settings of the "dockerRun" section (or any other way),
# and pass into the docker container:
# tasks.json:
# ...
# "dockerRun": {
# "containerName": "madiator2011-better-launcher", // no "/" allowed here for container name
# "image": "madiator2011/better-launcher:dev",
# "envFiles": ["${workspaceFolder}/.env"], // pass additional env-vars (hf_token, civitai token, ssh public-key) from ".env" file to container
# "env": { // this ENV vars go into the docker container to support local debugging
# "LOCAL_DEBUG": "True", // change app to localhost Urls and local Websockets (unsecured)
# "FLASK_APP": "app/app.py",
# "FLASK_ENV": "development", // changed from "production"
# "GEVENT_SUPPORT": "True" // gevent monkey-patching is being used, enable gevent support in the debugger
# // "FLASK_DEBUG": "0" // "1" allows debugging in Chrome, but then VSCode debugger not works
# },
# "volumes": [
# {
# "containerPath": "/app",
# "localPath": "${workspaceFolder}" // the "/app" folder (and sub-folders) will be mapped locally for debugging and hot-reload
# },
# {
# "containerPath": "/workspace",
# // TODO: create this folder before you run!
# "localPath": "${userHome}/Projects/Docker/Madiator/workspace"
# }
# ],
# "ports": [
# {
# "containerPort": 7222, // main Flask app port "AppManager"
# "hostPort": 7222
# },
# ...
#
#
# NOTE: to use the "LOCAL_DEBUG" ENV var just for local consumption of this image, you can run it like
#
# docker run -it -d --name madiator-better-launcher -p 22:22 -p 7777:7777 -p 7222:7222 -p 3000:3000 -p 7862:7862 -p 7863:7863
# -e LOCAL_DEBUG="True" -e RUNPOD_PUBLIC_IP="127.0.0.1" -e RUNPOD_TCP_PORT_22="22"
# -e PUBLIC_KEY="ssh-ed25519 XXXXXXX...XXXXX user@machine-DNS.local"
# --mount type=bind,source=/Users/test/Projects/Docker/madiator/workspace,target=/workspace
# madiator2011/better-launcher:dev
#
# To run the full 'app.py' / 'index.html' webserver locally, is was needed to "patch" the
# '/app/app.py' main application file and the
# '/app/templates/index.html'file according to the "LOCAL_DEBUG" ENV var,
# to switch the CF "proxy.runpod.net" Url for DEBUG:
#
### '/app/app.py' CHANGES ###:
# # lutzapps - CHANGE #1
# LOCAL_DEBUG = os.environ.get('LOCAL_DEBUG', 'False') # support local browsing for development/debugging
# ...
# filebrowser_status = get_filebrowser_status()
# return render_template('index.html',
# apps=app_configs,
# app_status=app_status,
# pod_id=RUNPOD_POD_ID,
# RUNPOD_PUBLIC_IP=os.environ.get('RUNPOD_PUBLIC_IP'),
# RUNPOD_TCP_PORT_22=os.environ.get('RUNPOD_TCP_PORT_22'),
# # lutzapps - CHANGE #2 - allow localhost Url for unsecure "http" and "ws" WebSockets protocol,
# according to LOCAL_DEBUG ENV var (used 3x in "index.html" changes)
# enable_unsecure_localhost=os.environ.get('LOCAL_DEBUG'),
# ...
# other (non-related) app.py changes omitted here
#
### '/app/template/index.html' CHANGES ###:
# <script>
# ...
# // *** lutzapps - Change #2 - support to run locally at http://localhost:${WS_PORT} (3 locations in "index.html")
# const enable_unsecure_localhost = '{{ enable_unsecure_localhost }}';
#
# // default is to use the "production" WeckSockets CloudFlare URL
# // NOTE: ` (back-ticks) are used here for template literals
# var WS_URL = `wss://${podId}-${WS_PORT}.proxy.runpod.net/ws`; // need to be declared as var
#
# if `${enable_unsecure_localhost}` === 'True') { // value of LOCAL_DEBUG ENV var
# // make sure to use "ws" Protocol (insecure) instead of "wss" (WebSockets Secure) for localhost,
# // otherwise you will get the 'loadingOverlay' stay and stays on screen with ERROR:
# // "WebSocket disconnected. Attempting to reconnect..." blocking the webpage http://localhost:7222
# WS_URL = `ws://localhost:${WS_PORT}/ws`; // localhost WS (unsecured)
# //alert(`Running locally with WS_URL=${WS_URL}`);
# }
# ...
# function openApp(appKey, port) {
# // *** lutzapps - Change #3 - support to run locally
# // NOTE: ` (back-ticks) are used here for template literals
# var url = `https://${podId}-${port}.proxy.runpod.net/`; // need to be declared as var
# if `${enable_unsecure_localhost}` === 'True') {
# url = `http://localhost:${port}/`; // remove runpod.net proxy
# //alert(`openApp URL=${url}`);
# }
#
# window.open(url, '_blank');
# }
# ...
# function openFileBrowser() {
# const podId = '{{ pod_id }}';
#
# // *** lutzapps - Change #5 - support to run locally
# // NOTE: ` (back-ticks) are used here for template literals
# var url = `https://${podId}-7222.proxy.runpod.net/fileapp/`; // need to be declared as var
# if `${enable_unsecure_localhost}` === 'True') {
# url = `http://localhost:7222/fileapp/`; // remove runpod.net proxy
# //alert(`FileBrowser URL=${url}`);
# }
#
# window.open(url, '_blank');
# }
# other (non-related) index.html changes omitted here
README_FILE_PREFIX = " _readme- " # prefix for all different dynamically generated README file names
### support local docker container runs with locally BOUND Workspace, needed also during local debugging
SHARED_MODELS_DIR = " /workspace/shared_models " # storage root for all shared_models (as designed for production app)
# check for "DISABLE_PULLBACK_MODELS" ENV var, and convert it from String to Boolean
# DISABLE_PULLBACK_MODELS: if not present (or empty) then "pull-back" local model files is enabled [default]
# else local found model files in app model folders will NOT be pulled back (and re-shared) into 'shared_models'
DISABLE_PULLBACK_MODELS = False
disable_pullback = os . environ . get ( ' DISABLE_PULLBACK_MODELS ' , ' False ' ) . lower ( ) # "True" / "true" / "1", or "False" / "false" / "0" / "" (not set)
if not ( disable_pullback == " " or disable_pullback == " false " or disable_pullback == " 0 " ) :
DISABLE_PULLBACK_MODELS = True
# check for "DISABLE_PULLBACK_MODELS" ENV var, and convert it from String to Boolean
# LOCAL_DEBUG: if not present (or empty) then run in "production" [default],
# else run as "development" / "debug" version
LOCAL_DEBUG = False
local_debug_str = os . environ . get ( ' LOCAL_DEBUG ' , ' False ' ) . lower ( ) # "True" / "true" / "1", or "False" / "false" / "0" / "" (not set)
if not ( local_debug_str == " " or local_debug_str == " false " or local_debug_str == " 0 " ) :
LOCAL_DEBUG = True
# show current configuration
print ( " \n \n *** SHARED_MODELS module init *** \n " )
print ( f " SHARED_MODELS_DIR= ' { SHARED_MODELS_DIR } ' \n " )
print ( f " LOCAL_DEBUG= ' { LOCAL_DEBUG } ' " )
print ( f " DISABLE_PULLBACK_MODELS= ' { DISABLE_PULLBACK_MODELS } ' " )
# show/hide the 3 dictionary MAPPING FILES, which control the whole module
MAKE_MAPPING_FILES_HIDDEN = False # can also be set according to LOCAL_DEBUG = True or False
if MAKE_MAPPING_FILES_HIDDEN :
HIDDEN_FILE_PREFIX = " . " # filenames starting with a dot (".") are hidden by the filesystem
else :
HIDDEN_FILE_PREFIX = " " # filenames are shown to the user
print ( f " MAKE_MAPPING_FILES_HIDDEN= ' { MAKE_MAPPING_FILES_HIDDEN } ' \n " )
# helper function to return a pretty formatted DICT string for human consumption (Logs, JSON)
def PrettyDICT ( dict : dict ) - > str :
dict_string = json . dumps ( dict , ensure_ascii = False , indent = 4 , separators = ( ' , ' , ' : ' ) )
return dict_string
# helper function called by init_shared_model_app_map() and init_shared_models_folders()
def write_dict_to_jsonfile ( dict : dict , json_filepath : str , overwrite : bool = False ) - > bool :
# Convert the 'dict' to JSON, and write the JSON object to file 'json_filepath'
#json_string = json.dumps(dict, indent=4, ensure_ascii=False, sort_keys=True)
try :
if os . path . exists ( json_filepath ) and not overwrite :
error_msg = f " dictionary file ' { json_filepath } ' already exists (and overwrite= { overwrite } ) "
#print(error_msg)
return False , error_msg # failure
# Write the JSON data to a file
with open ( json_filepath , ' w ' , encoding = ' utf-8 ' ) as output_file :
json . dump ( dict , output_file , ensure_ascii = False , indent = 4 , separators = ( ' , ' , ' : ' ) )
except Exception as e :
error_msg = f " ERROR in shared_models:write_dict_to_jsonfile() - loading JSON Map File ' { json_filepath } ' \n Exception: { str ( e ) } "
print ( error_msg )
return False , error_msg # failure
return True , " " # success
# helper function called by init_shared_model_app_map() and init_shared_models_folders()
def read_dict_from_jsonfile ( json_filepath : str ) - > dict :
# Read JSON file from 'json_filepath' and return it as 'dict'
try :
if os . path . exists ( json_filepath ) :
with open ( json_filepath , ' r ' ) as input_file :
dict = json . load ( input_file )
else :
error_msg = f " dictionary file ' { json_filepath } ' does not exist "
#print(error_msg)
return { } , error_msg # failure
except Exception as e :
error_msg = f " ERROR in shared_models:read_dict_from_jsonfile() - loading JSON Map File ' { json_filepath } ' \n Exception: { str ( e ) } "
print ( error_msg )
return { } , error_msg # failure
return dict , " " # success
# helper function for "init_app_install_dirs(), "init_shared_model_app_map()" and "init_shared_models_folders()"
def init_global_dict_from_file ( dict : dict , dict_filepath : str , dict_description : str ) - > bool :
# load or initialize the 'dict' for 'dict_description' from 'dict_filepath'
try :
if not os . path . exists ( SHARED_MODELS_DIR ) :
print ( f " \n The SHARED_MODELS_DIR ' { SHARED_MODELS_DIR } ' is not found! \n Create it by clicking the ' Create Shared Folders ' button from the WebUI ' Settings ' Tab \n " )
return
if os . path . isfile ( dict_filepath ) and os . path . exists ( dict_filepath ) :
dict_filepath_found = True
# read the dict_description from JSON file
print ( f " \n Existing ' { dict_description } ' found and read from file ' { dict_filepath } ' \n The file overwrites the code defaults! " )
dict , error_msg = read_dict_from_jsonfile ( dict_filepath )
if not error_msg == " " :
print ( error_msg )
else : # init the dict_description from app code
dict_filepath_found = False
print ( f " No ' { dict_description } ' _FILE found, initializing default ' { dict_description } ' from code ... " )
# use already defined dict from app code
# write the dict to JSON file
success , ErrorMsg = write_dict_to_jsonfile ( dict , dict_filepath )
if success :
print ( f " ' { dict_description } ' is initialized and written to file ' { dict_filepath } ' " )
else :
print ( ErrorMsg )
# Convert 'dict_description' dictionary to formatted JSON
print ( f " \n Using { ' external ' if dict_filepath_found else ' default ' } ' { dict_description } ' : \n { PrettyDICT ( dict ) } " )
except Exception as e :
error_msg = f " ERROR in shared_models:init_global_dict_from_file() - initializing dict Map File ' { dict_filepath } ' \n Exception: { str ( e ) } "
print ( error_msg )
return False , error_msg
return True , " " # success
# the below SHARED_MODEL_FOLDERS_FILE will be read and used (if exists),
# otherwise this file will be generated with the content of the below default SHARED_MODEL_FOLDERS dict
SHARED_MODEL_FOLDERS_FILE = f " { SHARED_MODELS_DIR } / { HIDDEN_FILE_PREFIX } _shared_model_folders.json "
SHARED_MODEL_FOLDERS = {
# "model_type" (=subdir_name of SHARED_MODELS_DIR): "model_type_description"
" ckpt " : " Model Checkpoint (Full model including a CLIP and VAE model) " ,
" clip " : " CLIP Model (used together with UNET models) " ,
" controlnet " : " ControlNet model (Canny, Depth, Hed, OpenPose, Union-Pro, etc.) " ,
" embeddings " : " Embedding (aka Textual Inversion) Model " ,
" hypernetworks " : " HyperNetwork Model " ,
" insightface " : " InsightFace Model " ,
" ipadapters " : " ControlNet IP-Adapter Model " ,
" ipadapters/xlabs " : " IP-Adapter from XLabs-AI " ,
" LLM " : " LLM (aka Large-Language Model) is folder mapped (1 folder per model), append ' /* ' in the map " ,
" loras " : " LoRA (aka Low-Ranking Adaption) Model " ,
" loras/xlabs " : " LoRA Model from XLabs-AI " ,
" loras/flux " : " LoRA Model trained on Flux.1 Dev or Flux.1 Schnell " ,
" reactor " : " Reactor Model " ,
" reactor/faces " : " Reactor Face Model " ,
" unet " : " UNET Model Checkpoint (need separate CLIP and VAE Models) " ,
" upscale_models " : " Upscaling Model (based on ESRGAN) " ,
" vae " : " VAE En-/Decoder Model " ,
" vae-approx " : " Approximate VAE Model "
}
# helper function called by "inline"-main() and ensure_shared_models_folders()
def init_shared_models_folders ( send_SocketMessage : bool = True ) :
global SHARED_MODEL_FOLDERS
init_global_dict_from_file ( SHARED_MODEL_FOLDERS , SHARED_MODEL_FOLDERS_FILE , " SHARED_MODEL_FOLDERS " )
if os . path . exists ( SHARED_MODEL_FOLDERS_FILE ) and send_SocketMessage :
2024-10-30 20:22:42 +07:00
send_websocket_message ( ' extend_ui_helper ' , {
' cmd ' : ' refreshModelTypes ' ,
2024-10-27 03:15:28 +07:00
' message ' : ' New ModelTypes are available '
2024-10-30 20:22:42 +07:00
} )
2024-10-27 03:15:28 +07:00
return
### "inline"-main() ###
# init the SHARED_MODEL_FOLDERS
2024-10-30 20:22:42 +07:00
init_shared_models_folders ( False ) # dont send a WS-Message for "Model Downloader" at module init, to init/refresh its modelType list
2024-10-27 03:15:28 +07:00
# ----------
2024-10-30 20:22:42 +07:00
# helper function called from "app.py" via WebUI "Create Shared Folders" button on "Settings" tab
2024-10-27 03:15:28 +07:00
# ensures 'model_type' sub-folders for Model Mapping and the "Model Downloader" exists
# in the SHARED_MODELS_DIR (uses above initialized 'SHARED_MODEL_FOLDERS' dict)
def ensure_shared_models_folders ( ) :
try :
# init global module 'SHARED_MODEL_FOLDERS' dict: { 'model_type' (=subdir_names): 'app_model_dir'
2024-10-30 20:22:42 +07:00
# from app code or from external JSON 'SHARED_MODEL_FOLDERS_FILE' file
init_shared_models_folders ( False ) # (re-)read the SHARED_MODEL_FOLDERS_FILE again, if changed, but don't refresh modelTypes in "Model Downloader" yet
2024-10-27 03:15:28 +07:00
print ( f " (re-)creating ' shared_models ' model type sub-folders for Apps and the ' Model Downloader ' in folder ' { SHARED_MODELS_DIR } ' : " )
2024-10-30 20:22:42 +07:00
# create the shared_models directory, if it doesn't exist yet
2024-10-27 03:15:28 +07:00
os . makedirs ( f " { SHARED_MODELS_DIR } / " , exist_ok = True ) # append slash to make sure folder is created
# create a "__README.txt" file in the shared_models directory
readme_path = os . path . join ( SHARED_MODELS_DIR , ' __README.txt ' )
with open ( readme_path , ' w ' ) as readme_file :
readme_file . write ( " Upload your models to the appropriate folders: \n \n " )
for model_type , model_type_description in SHARED_MODEL_FOLDERS . items ( ) :
shared_model_folderpath = os . path . join ( SHARED_MODELS_DIR , model_type )
2024-10-30 20:22:42 +07:00
2024-10-27 03:15:28 +07:00
os . makedirs ( os . path . dirname ( f " { shared_model_folderpath } / " ) , exist_ok = True ) # append trailing "/" to make sure the last sub-folder is created
print ( f " ' { model_type } ' Folder created for ' { model_type_description } ' " )
model_type_name = model_type
if model_type_name . endswith ( ' s ' ) : # model_type uses "plural" form with trailing "s"
model_type_name = model_type [ : - 1 ] # cut the last trailing 's'
readme_model_type_filename = os . path . join ( shared_model_folderpath , f " { README_FILE_PREFIX } { model_type . replace ( ' / ' , ' - ' ) } .txt " ) # translate "/" from grouping map rule into valid readme filename, e.g. "loras/flux" into "loras-flux"
readme_model_type_file = open ( readme_model_type_filename , ' w ' )
readme_model_type_file . writelines ( f " Put your ' { model_type_name } ' type models here, { model_type_description } " )
readme_model_type_file . close ( )
readme_file . write ( f " - { model_type } : for { model_type_name } models, { model_type_description } \n " )
readme_file . write ( " \n These models will be automatically linked to all supported apps. \n \n " )
readme_file . write ( " Models directly downloaded into an app model folder will be \n " )
readme_file . write ( " automatically pulled back into the corresponding shared folder and relinked back! \n " )
2024-10-30 20:22:42 +07:00
# send a message for the "Model Downloader" to "refresh" its 'modelType' list
send_websocket_message ( ' extend_ui_helper ' , {
' cmd ' : ' refreshModelTypes ' ,
' message ' : ' New ModelTypes are available '
} )
2024-10-27 03:15:28 +07:00
return jsonify ( { ' status ' : ' success ' , ' message ' : ' Shared model folders created successfully. ' } )
except Exception as e :
print ( f " ERROR in shared_models:ensure_shared_model_folder() - Exception: \n { str ( e ) } " )
return jsonify ( { ' status ' : ' error ' , ' message ' : str ( e ) } )
# ----------
# the below APP_INSTALL_DIRS_FILE will be read and used (if exists),
# otherwise this file will be generated with the content of the below default APP_INSTALL_DIRS dict
# this dict is very important, as it "defines" part of the symlink path,
# together with below defined SHARED_MODEL_APP_MAP (which uses relative path to the "app_install_dir" aka 'app_path')
APP_INSTALL_DIRS_FILE = f " { SHARED_MODELS_DIR } / { HIDDEN_FILE_PREFIX } _app_install_dirs.json "
APP_INSTALL_DIRS = {
# "app_name": "app_install_dir"
" A1111 " : " /workspace/stable-diffusion-webui " ,
" Forge " : " /workspace/stable-diffusion-webui-forge " ,
" ComfyUI " : " /workspace/ComfyUI " ,
" Kohya_ss " : " /workspace/Kohya_ss " ,
" CUSTOM1 " : " /workspace/joy-caption-batch "
}
# the code from Madiator also defines a similar 'app_configs' dictionary
# but the idea here is also to allow "CUSTOM" Apps, installed by the user manually,
# to participate in "shared_models" model sharing
# app_configs = {
# 'bcomfy': {
# 'name': 'Better Comfy UI',
# 'command': 'cd /workspace/bcomfy && . ./bin/activate && cd /workspace/ComfyUI && python main.py --listen --port 3000 --enable-cors-header',
# 'venv_path': '/workspace/bcomfy',
# 'app_path': '/workspace/ComfyUI',
# 'port': 3000,
# },
# 'bforge': {
# 'name': 'Better Forge',
# 'command': 'cd /workspace/bforge && . ./bin/activate && cd /workspace/stable-diffusion-webui-forge && ./webui.sh -f --listen --enable-insecure-extension-access --api --port 7862',
# 'venv_path': '/workspace/bforge',
# 'app_path': '/workspace/stable-diffusion-webui-forge',
# 'port': 7862,
# },
# 'ba1111': {
# 'name': 'Better A1111',
# 'command': 'cd /workspace/ba1111 && . ./bin/activate && cd /workspace/stable-diffusion-webui && ./webui.sh -f --listen --enable-insecure-extension-access --api --port 7863',
# 'venv_path': '/workspace/ba1111',
# 'app_path': '/workspace/stable-diffusion-webui',
# 'port': 7863,
# }
# }
# MAP between Madiator's "app_configs" dict and the "APP_INSTALL_DIRS" dict used in this module
# TODO: this is temporary and should be merged/integrated better later
MAP_APPS = {
" bcomfy " : " ComfyUI " ,
" bforge " : " Forge " ,
" ba1111 " : " A1111 "
}
# helper function called by main(), uses above "MAP_APPS" dict
# TODO: this is temporary and should be merged/integrated better later
def sync_with_app_configs_install_dirs ( ) :
print ( f " Syncing ' app_configs ' dict ' app_path ' into the ' APP_INSTALL_DIRS ' dict ... " )
app_configs = get_app_configs ( )
for bapp_name , config in app_configs . items ( ) :
if bapp_name in MAP_APPS :
# get/sync the bapp_path from app_configs dict
bapp_path = app_configs [ bapp_name ] [ " app_path " ]
print ( f " \t Syncing ' app_path ' : ' { bapp_path } ' from app_configs for app ' name ' : ' { bapp_name } ' " )
APP_INSTALL_DIRS [ MAP_APPS [ bapp_name ] ] = bapp_path # update path in APP_INSTALL_DIRS
# show final synced APP_INSTALL_DIRS
print ( f " \n Using synched ' APP_INSTALL_DIRS ' : \n { PrettyDICT ( APP_INSTALL_DIRS ) } " )
# init global module 'APP_INSTALL_DIRS' dict: { 'app_name': 'app_installdir' }
# default mapping from app code or (if exists) from external JSON 'APP_INSTALL_DIRS_FILE' file
# NOTE: this APP_INSTALL_DIRS_FILE is temporary synced with the app_configs dict
def init_app_install_dirs ( ) :
global APP_INSTALL_DIRS
init_global_dict_from_file ( APP_INSTALL_DIRS , APP_INSTALL_DIRS_FILE , " APP_INSTALL_DIRS " )
return
### "inline"-main() ###
# init the APP_INSTALL_DIRS and sync it from "app_configs" dict
init_app_install_dirs ( )
sync_with_app_configs_install_dirs ( ) # TODO: this is temporary and should be merged/integrated better later
# ----------
SHARED_MODEL_APP_MAP_FILE = f " { SHARED_MODELS_DIR } / { HIDDEN_FILE_PREFIX } _shared_model_app_map.json "
# The dictionary 'model_type' "key" is relative to the SHARED_MODELS_DIR "/workspace/shared_models/" main folder.
# The sub dictionary 'app_model_folderpath' value is relative to the 'app_install_dir' value
# of the above APP_INSTALL_DIRS dictionary
# here is a list of all "known" model type dirs, and if they are used here (mapped) or
# if they are currently "unmapped":
#
# "Kohya_ss" (mapped): "/models"
# "ComfyUI" (mapped): "/models/checkpoints", "/models/clip", "/models/controlnet", "/models/embeddings", "/models/hypernetworks", "/models/ipadapter/"(???), "/models/loras", "/models/reactor"(???), "/models/unet", "/models/upscale_models", "/models/vae", "/models/vae_approx"
# "ComfyUI" (unmapped): "/models/clip_vision", "/models/diffusers", "/models/diffusion_models", "/models/gligen", "/models/photomaker", "/moedls/style_models",
# "A1111"/"Forge" (mapped): "/embeddings", "/models/ControlNet", "/models/ESRGAN", "/models/hypernetworks", "/models/insightface"(???), "/models/Lora", "/models/reactor", "/models/Stable-diffusion", "/models/text_encoder"(???), "/models/VAE", "/models/VAE-approx"
# "A1111"/"Forge" (unmapped): "/model/adetailer", "/models/BLIP", "/models/Codeformer", "models/deepbooru", "/model/Deforum", "/models/GFPGAN", "/models/karlo", "/models/Unet-onnx", "/models/Unet-trt"
SHARED_MODEL_APP_MAP = {
" ckpt " : { # "model_type" (=subdir_name of SHARED_MODELS_DIR)
# "app_name": "app_model_folderpath" (for this "model_type", path is RELATIVE to "app_install_dir" of APP_INSTALL_DIRS map)
" ComfyUI " : " /models/checkpoints " ,
" A1111 " : " /models/Stable-diffusion " ,
" Forge " : " /models/Stable-diffusion " ,
" Kohya_ss " : " /models " # flatten all "ckpt" / "unet" models here
} ,
" clip " : {
" ComfyUI " : " /models/clip " ,
" A1111 " : " /models/text_encoder " ,
" Forge " : " /models/text_encoder "
} ,
" controlnet " : {
" ComfyUI " : " /models/controlnet " ,
" A1111 " : " /models/ControlNet " ,
" Forge " : " /models/ControlNet "
#"A1111": "/extensions/sd-webui-controlnet/models", # SD1.5 ControlNets
#"Forge": "/extensions/sd-webui-controlnet/models" # SD1.5 ControlNets
} ,
# EMBEDDINGS map outside of models folder for FORGE / A1111
" embeddings " : {
" ComfyUI " : " /models/embeddings " ,
" A1111 " : " /embeddings " ,
" Forge " : " /embeddings "
} ,
" hypernetworks " : {
" ComfyUI " : " /models/hypernetworks " ,
" A1111 " : " /models/hypernetworks " ,
" Forge " : " /models/hypernetworks "
} ,
" insightface " : {
" ComfyUI " : " /models/insightface " ,
" A1111 " : " /models/insightface " , # unverified location
" Forge " : " /models/insightface " # unverified location
} ,
" ipadapters " : {
" ComfyUI " : " /models/ipadapter/ " ,
" A1111 " : " /extensions/sd-webui-controlnet/models " , # unverified location
" Forge " : " /extensions/sd-webui-controlnet/models " # unverified location
} ,
" ipadapters/xlabs " : { # sub-folders for XLabs-AI IP-Adapters
" ComfyUI " : " /models/xlabs/ipadapters " ,
" A1111 " : " /extensions/sd-webui-controlnet/models " , # flatten all "xlabs" ipadapters here
" Forge " : " /extensions/sd-webui-controlnet/models " # flatten all "xlabs" ipadapters here
} ,
# some LoRAs get stored here in sub-folders, e.g. "/xlabs/*"
" loras " : {
" ComfyUI " : " /models/loras " ,
" A1111 " : " /models/Lora " ,
" Forge " : " /models/Lora "
} ,
# Support "XLabs-AI" LoRA models
" loras/xlabs " : { # special syntax for "grouping"
" ComfyUI " : " /models/loras/xlabs " ,
" A1111 " : " /models/Lora " , # flatten all "xlabs" LoRAs here
" Forge " : " /models/Lora " # flatten all "xlabs" LoRAs here
} ,
# Support "Grouping" all FLUX LoRA models into a LoRA "flux" sub-folder for ComfyUI,
# which again need to be flattened for other apps
" loras/flux " : {
" ComfyUI " : " /models/loras/flux " ,
" A1111 " : " /models/Lora " , # flatten all "flux" LoRAs here
" Forge " : " /models/Lora " # flatten all "flux" LoRAs here
} ,
" reactor " : {
" ComfyUI " : " /models/reactor " , # unverified location
" A1111 " : " /models/reactor " ,
" Forge " : " /models/reactor " ,
} ,
" reactor/faces " : {
" ComfyUI " : " /models/reactor/faces " , # unverified location
" A1111 " : " /models/reactor " ,
" Forge " : " /models/reactor " ,
} ,
# UNET models map into the CKPT folders of all other apps, except for ComfyUI
" unet " : {
" ComfyUI " : " /models/unet " ,
" A1111 " : " /models/Stable-diffusion " , # flatten all "ckpts" / "unet" models here
" Forge " : " /models/Stable-diffusion " , # flatten all "ckpts" / "unet" models here
" Kohya_ss " : " /models " # flatten all "ckpt" / "unet" models here
} ,
" upscale_models " : {
" ComfyUI " : " /models/upscale_models " ,
" A1111 " : " /models/ESRGAN " ,
" Forge " : " /models/ESRGAN "
} ,
" vae " : {
" ComfyUI " : " /models/vae " ,
" A1111 " : " /models/VAE " ,
" Forge " : " /models/VAE "
} ,
" vae-approx " : {
" ComfyUI " : " /models/vae_approx " ,
" A1111 " : " /models/VAE-approx " ,
" Forge " : " /models/VAE-approx "
} ,
# E.g. Custom Apps support for Joytag-Caption-Batch Tool (which uses the "Meta-Llama-3.1-8B" LLM)
# to share the model with e.g. ComfyUI. This LLM model come as full folders with more than one file!
# Pay attention to the special syntax for folder mappings (add a "/*" suffix to denote a folder mapping)
" LLM/Meta-Llama-3.1-8B/* " : { # special syntax for "folder" symlink (the "/*" is mandatory)
" ComfyUI " : " /models/LLM/Meta-Llama-3.1-8B/* " , # special syntax for "folder" symlink, the "/*" is optional
" CUSTOM1 " : " /model/* " # special syntax for "folder" symlink, the "/*" is optional
}
}
# the "init_shared_model_app_map()" function initializes the
# global module 'SHARED_MODEL_APP_MAP' dict: 'model_type' -> 'app_name:app_model_dir' (relative path)
# which does a default mapping from app code or (if exists) from external JSON 'SHARED_MODEL_APP_MAP_FILE' file
def init_shared_model_app_map ( ) :
global SHARED_MODEL_APP_MAP
init_global_dict_from_file ( SHARED_MODEL_APP_MAP , SHARED_MODEL_APP_MAP_FILE , " SHARED_MODEL_APP_MAP " )
return
### "inline"-main() ###
# init the SHARED_MODEL_APP_MAP
init_shared_model_app_map ( )
# ----------
# helper function called by update_model_symlinks()
def remove_broken_model_symlinks ( shared_model_folderpath : str , app_model_folderpath : str , model_type : str ) - > int :
# process all files in app_model_folderpath
print ( f " -> process broken ' { model_type } ' app_model file symlinks, which where removed from their corresponding shared_models sub-folder ... " )
broken_modellinks_count = 0
broken_modellinks_info = " "
for app_model_filename in os . listdir ( app_model_folderpath ) :
app_model_filepath = os . path . join ( os . path . join ( app_model_folderpath , app_model_filename ) )
# check for stale/broken model filelinks and folderlinks (LLMs)
if os . path . islink ( app_model_filepath ) and not os . path . exists ( app_model_filepath ) :
# Remove existing stale/broken symlink
broken_modellinks_count = broken_modellinks_count + 1
dateInfo = " { : % B %d , % Y, % H: % M: % S GMT} " . format ( datetime . datetime . now ( ) )
broken_modellinks_info + = f " \t { app_model_filename } \t [@ { dateInfo } ] \n "
os . unlink ( app_model_filepath ) # try to unlink the file/folder symlink
# that normally is enough to remove the broken link (and the below code may never run)
# re-check if file/folder still exists
if os . path . exists ( app_model_filepath ) : # if file/folder link still exists
if os . path . isdir ( app_model_filepath ) : # broken folder link
shutil . rmtree ( app_model_filepath ) # remove the linked folder
else : # broken file link
os . remove ( app_model_filepath ) # remove the file link
print ( f " \t removed broken symlink for model ' { app_model_filepath } ' " )
if broken_modellinks_count > 0 :
# maintain (create/append to) a readme file for the app_model_folderpath target folder about the removed/deleted Model File Symlinks
readme_brokenlinks_models_filepath = os . path . join ( app_model_folderpath , f " { README_FILE_PREFIX } brokenlinks- { model_type . replace ( ' / ' , ' - ' ) } .txt " ) # translate "/" from grouping map rule into valid readme filename, e.g. "loras/flux" into "loras-flux"
if not os . path . exists ( readme_brokenlinks_models_filepath ) : # no such readme file exists, so create it
fileHeader = f " Following broken model file links have been found and where deleted from this directory: \n \n "
file = open ( readme_brokenlinks_models_filepath , ' w ' ) # create the file
file . writelines ( fileHeader ) # and write the fileHeader once
file . writelines ( broken_modellinks_info ) # and add the broken Model File Links
file . close ( )
else : # readme file already existed from before
file = open ( readme_brokenlinks_models_filepath , ' a ' ) # append to file
file . writelines ( broken_modellinks_info ) # and add the broken Model File Links
file . close ( )
return broken_modellinks_count
# helper function called by update_model_symlinks()
def pull_unlinked_models_back_as_shared_models ( shared_model_folderpath : str , app_model_folderpath : str , model_type : str ) - > int :
# process all files in app_model_folderpath
print ( f " -> process for possibly pulling-back ' { model_type } ' local app_model files into their corresponding shared_models sub-folder ... " )
pulled_model_files_count = 0
pulled_model_files_info = " "
for app_model_filename in os . listdir ( app_model_folderpath ) :
if app_model_filename . startswith ( " . " ) or app_model_filename . startswith ( README_FILE_PREFIX ) :
continue # skip hidden filenames like ".DS_Store" (on macOS), ".keep" (on GitHub) and all "{README_FILE_PREFIX}*.txt" files
app_model_filepath = os . path . join ( app_model_folderpath , app_model_filename )
if os . path . islink ( app_model_filepath ) or os . path . isdir ( app_model_filepath ) :
continue # skip all already symlinked model files and sub-folders
# real file, potentially a model file which can be pulled back "home"
pulled_model_files_count = pulled_model_files_count + 1
print ( f " processing app model ' { app_model_filename } ' ... " )
shared_model_filepath = os . path . join ( shared_model_folderpath , app_model_filename )
print ( f " moving the file ' { app_model_filename } ' back to the ' { model_type } ' shared_models folder " )
shutil . move ( app_model_filepath , shared_model_filepath ) # move it back to the shared_models model type folder
print ( f " \t pulled-back local model ' { app_model_filepath } ' " )
dateInfo = " { : % B %d , % Y, % H: % M: % S GMT} " . format ( datetime . datetime . now ( ) )
pulled_model_files_info + = f " \t { app_model_filename } \t [@ { dateInfo } ] \n "
### and re-link it back to this folder where it got just pulled back
# get the full path from shared model filename
src_filepath = os . path . join ( shared_model_folderpath , app_model_filename )
dst_filepath = os . path . join ( app_model_folderpath , app_model_filename )
if os . path . isfile ( src_filepath ) and not os . path . exists ( dst_filepath ) :
os . symlink ( src_filepath , dst_filepath )
print ( f " \t re-created symlink { app_model_filename } -> { src_filepath } for pulled model " )
if pulled_model_files_count > 0 :
# maintain (create/append to) a readme file for the app_model_folderpath target folder about the pulled Model Files
readme_pulled_models_filepath = os . path . join ( app_model_folderpath , f " { README_FILE_PREFIX } pulled- { model_type . replace ( ' / ' , ' - ' ) } .txt " ) # translate "/" from grouping map rule into valid readme filename, e.g. "loras/flux" into "loras-flux"
if not os . path . exists ( readme_pulled_models_filepath ) : # no such readme file exists, so create it
fileHeader = f " Following model files have been pulled from this directory into the shared_models directory ' { shared_model_folderpath } ' and re-linked here: \n \n "
file = open ( readme_pulled_models_filepath , ' w ' ) # create the file
file . writelines ( fileHeader ) # and write the fileHeader once
file . writelines ( pulled_model_files_info ) # and add the pulled Model Files
file . close ( )
else : # readme file already existed from before
file = open ( readme_pulled_models_filepath , ' a ' ) # append to file
file . writelines ( pulled_model_files_info ) # and add the pulled Model Files
file . close ( )
return pulled_model_files_count
# helper function called by update_model_symlinks()
def create_model_symlinks ( shared_model_folderpath : str , app_model_folderpath : str , model_type : str ) - > int :
# process all files in shared_model_folderpath
print ( f " -> process for creating ' { model_type } ' app_model file symlinks from their corresponding shared_models sub-folder ... " )
file_symlinks_created_count = 0
for shared_model_filename in os . listdir ( shared_model_folderpath ) :
if shared_model_filename . startswith ( " . " ) :
continue # skip hidden filenames like ".DS_Store" (on macOS), ".keep" (on GitHub)
# change the "readme-*.txt" files for the symlinked app folder models
if shared_model_filename . startswith ( README_FILE_PREFIX ) :
# create a new readme file for the app_model_folderpath target folder
readme_synched_filename = os . path . join ( app_model_folderpath , shared_model_filename . replace ( README_FILE_PREFIX , f ' { README_FILE_PREFIX } synced- ' ) )
os . makedirs ( os . path . dirname ( readme_synched_filename ) , exist_ok = True ) # ensure parent directory exists
file = open ( readme_synched_filename , ' w ' )
file . writelines ( f " This folder is synced from the shared_models ' { model_type } ' models type sub-folder at ' { shared_model_folderpath } ' . \n \n Consider to put such models there to share them across apps, instead of putting them here! " )
file . close ( )
continue # skip the original "{README_FILE_PREFIX}*.txt" file
print ( f " \t processing shared ' { model_type } ' model ' { shared_model_filename } ' ... " )
# get the full path from shared model filename
src_filepath = os . path . join ( shared_model_folderpath , shared_model_filename )
dst_filepath = os . path . join ( app_model_folderpath , shared_model_filename ) # the dst_filepath always has the SAME filename as the src_filepath
if not os . path . isfile ( src_filepath ) : # srcFile is a sub-folder (e.g. "xlabs", or "flux")
# skip sub-folders, as these require a separate mapping rule to support "flattening" such models
# for apps which don't find their model_type models in sub-folders
# add a "model map" for "loras/flux" like the following:
# Support "Grouping" all FLUX LoRA models into a LoRA "flux" sub-folder for ComfyUI,
# which again need to be flattened for other apps
# "loras/flux": {
# "ComfyUI": "/models/loras/flux",
# "A1111": "/models/Lora", # flatten all "flux" LoRAs here
# "Forge": "/models/Lora" # flatten all "flux" LoRAs here
# }
print ( f " \t this is a sub-folder which should be mapped with a ' grouping ' rule, \n \t e.g. " " {model_type} / {shared_model_filename} : { ... } " " in ' {SHARED_MODEL_APP_MAP_FILE} ' " )
continue
# create dst_filepath dirs for the parent folder, if needed
os . makedirs ( os . path . dirname ( dst_filepath ) , exist_ok = True )
if os . path . isfile ( src_filepath ) and not os . path . exists ( dst_filepath ) :
os . symlink ( src_filepath , dst_filepath )
print ( f " \t created symlink { shared_model_filename } -> { src_filepath } " )
file_symlinks_created_count = file_symlinks_created_count + 1
return file_symlinks_created_count
# helper function called from "app.py" via WebUI
#
# this is the main WORKER function running every 5 minutes
# or "on-demand" by the user from the WebUI via app.py:recreate_symlinks_route()->recreate_symlinks()
#
# this function uses following global module vars:
#
# README_FILE_PREFIX (str): "_readme-" <- README files are put in shared model type dirs and also in the app model type dirs,
# e.g. "_readme-*.txt", "_readme-synced-*.txt", "_readme-pulled-*.txt", "_readme-brokenlinks-*.txt",
# the "*" is filled with the dir-name of the corresponding model type subfolder of the SHARED_MODELS_DIR (str)
#
# DISABLE_PULLBACK_MODELS (bool) <- set via ENV (True, or False if not present [default])
# LOCAL_DEBUG (bool) <- set via ENV (True, or False if not present [default])
# MAKE_MAPPING_FILES_HIDDEN (bool): default=False (currently only controlled by app code)
#
# SHARED_MODELS_DIR (str): "/workspace/shared_models"
#
# SHARED_MODEL_FOLDERS_FILE (str): "_shared_models_folders.json" (based in SHARED_MODELS_DIR)
# SHARED_MODEL_FOLDERS (dict) <- init from code, then write/read from path SHARED_MODEL_FOLDERS_FILE
#
# APP_INSTALL_DIRS_FILE (str): "_app_install_dirs.json" (based in SHARED_MODELS_DIR)
# APP_INSTALL_DIRS (dict) <- init from code, then write/read from path SHARED_MODEL_FOLDERS_FILE,
# -> synced with global "app_configs" dict for 'app_path' with the use of MAP_APPS (dict)
# MAP_APPS (dict) <- used for mapping "app_configs" (dict) with APP_INSTALL_DIRS (dict)
#
# SHARED_MODEL_APP_MAP_FILE (str): "_shared_models_map.json" (based in SHARED_MODELS_DIR)
# SHARED_MODEL_APP_MAP (dict) <- init from code, then write/read from path SHARED_MODEL_FOLDERS_FILE
2024-10-30 20:22:42 +07:00
def update_model_symlinks ( ) - > dict :
2024-10-27 03:15:28 +07:00
try :
print ( f " Processing the master SHARED_MODELS_DIR: { SHARED_MODELS_DIR } " )
if not os . path . exists ( SHARED_MODELS_DIR ) :
message = f " Folder ' { SHARED_MODELS_DIR } ' does not exist, please create it first! "
return jsonify ( { ' status ' : ' error ' , ' message ' : message } )
file_model_symlinks_created_count = 0 # file model symlinks created
folder_model_symlinks_created_count = 0 # folder model symlinks created
broken_model_symlinks_count = 0 # broken symlinks to model files and folders
# "pull-back" model files can be disabled with ENV var "DISABLE_PULLBACK_MODELS=True"
pulled_model_files_count = 0 # pulled back model files (we not pull back folder models)
for model_type in SHARED_MODEL_APP_MAP :
print ( f " \n ### processing shared ' { model_type } ' model symlinks for all installed apps ... " )
# check for special LLM folder symlink syntax
if not model_type . endswith ( " /* " ) : # normal file symlink in regular app_model_folderpath folder
create_folder_symlink = False
shared_model_folderpath = os . path . join ( SHARED_MODELS_DIR , model_type )
else : # special case for folder symlink
create_folder_symlink = True
# strip the "/*" from the model_type (to deal with real folder names), before generating model_folderpaths
model_type_dirname = model_type . strip ( " /* " )
shared_model_folderpath = os . path . join ( SHARED_MODELS_DIR , model_type_dirname )
if not os . path . isdir ( shared_model_folderpath ) :
print ( f " shared_model_folderpath ' { model_type } ' does not exist, skipping " )
continue # skipping non-existant shared_model_folderpath SRC folders
for app_name , app_install_dir in APP_INSTALL_DIRS . items ( ) :
if not os . path . exists ( app_install_dir ) : # app is NOT installed
print ( f " \n ## app ' { app_name } ' is not installed, skipping " )
continue # skipping non-installed app_install_dir for this model_type
print ( f " \n ## processing for app ' { app_name } ' ... " )
if not ( app_name in SHARED_MODEL_APP_MAP [ model_type ] ) :
print ( f " -> there are no ' { model_type } ' symlink mappings defined for app ' { app_name } ' , skipping " )
continue # skipping non-existent app_name mapping for this model_type
app_model_folderpath = APP_INSTALL_DIRS [ app_name ] + SHARED_MODEL_APP_MAP [ model_type ] [ app_name ]
print ( f " # Processing the app ' s ' { model_type } ' folder ' { app_model_folderpath } ' ... " )
if not create_folder_symlink : # normal file symlink in regular app_model_folderpath folder
# create the app model_type directory, if it doesn't exist
os . makedirs ( f " { app_model_folderpath } / " , exist_ok = True ) # append slash to make sure folder is created
# first remove all broken/stale links
broken_model_type_symlinks_count = remove_broken_model_symlinks ( shared_model_folderpath , app_model_folderpath , model_type )
if broken_model_type_symlinks_count > 0 :
readme_filename = f " { README_FILE_PREFIX } brokenlinks- { model_type . replace ( ' / ' , ' - ' ) } .txt " # translate "/" from grouping map rule into valid readme filename, e.g. "loras/flux" into "loras-flux"
print ( f " -> found and removed # { broken_model_type_symlinks_count } broken link(s) for model type ' { model_type } ' \n for more info about which model files symlinks were removed, look into the ' { readme_filename } ' " )
# add them to its global counter
broken_model_symlinks_count = broken_model_symlinks_count + broken_model_type_symlinks_count
if not DISABLE_PULLBACK_MODELS :
# then try to pull back local, unlinked app models of this model type (they also get re-shared instantly back)
pulled_model_type_files_count = pull_unlinked_models_back_as_shared_models ( shared_model_folderpath , app_model_folderpath , model_type )
if pulled_model_type_files_count > 0 :
readme_filename = f " { README_FILE_PREFIX } pulled- { model_type . replace ( ' / ' , ' - ' ) } .txt " # translate "/" from grouping map rule into valid readme filename, e.g. "loras/flux" into "loras-flux"
print ( f " -> found and pulled back # { pulled_model_type_files_count } ' { model_type } ' model(s) into the corresponding shared_models sub-folder, \n for more info about which model files were pulled, look into the ' { readme_filename } ' " )
# add them to its global counter
pulled_model_files_count = pulled_model_files_count + pulled_model_type_files_count
# now share (symlink) all models of this type to app model path
file_model_type_symlinks_created_count = create_model_symlinks ( shared_model_folderpath , app_model_folderpath , model_type )
if file_model_type_symlinks_created_count > 0 :
# no readme details are tracked about created file symlinks
print ( f " -> created # { file_model_type_symlinks_created_count } model file symlinks for ' { model_type } ' model(s) \n you can see which models are now available in the app ' s ' { app_model_folderpath } ' " )
# add them to its global counter
file_model_symlinks_created_count = file_model_symlinks_created_count + file_model_type_symlinks_created_count
else : # special case for folder symlink with LLM models, which install as folder
# e.g. app_model_folderpath = "/workspace/ComfyUI/models/LLM/Meta-Llama-3.1-8B/*"
# normally the target mapped/symlinked folder should also use a "/*" suffix,
# but that is not stricly required as we can handle that
# strip the "/*" from the model-map, to create the a "real" target folder per folder symlink
app_model_folderpath = app_model_folderpath . strip ( " /* " )
# e.g. app_model_folderpath = "/workspace/ComfyUI/models/LLM/Meta-Llama-3.1-8B"
app_model_parent_dir , app_model_foldername = os . path . split ( app_model_folderpath )
# e.g. app_model_parent_dir = "/workspace/ComfyUI/models/LLM"
os . makedirs ( os . path . dirname ( f " { app_model_parent_dir } / " ) , exist_ok = True ) # append trailing "/" to make sure the last sub-folder is created
if os . path . exists ( shared_model_folderpath ) and not os . path . isfile ( shared_model_folderpath ) and not os . path . exists ( app_model_folderpath ) :
# create a folder symlink
os . symlink ( shared_model_folderpath , app_model_folderpath , target_is_directory = True )
# no readme details are tracked about created folder symlinks
print ( f " \t created a folder symlink { app_model_foldername } -> { shared_model_folderpath } " )
# the model_type counter for folder models is always 1, as each folder model is its own model_type
folder_symlinks_created_count = 1
# add this one folder symlink to its global (folder) counter (one-by-one)
folder_model_symlinks_created_count = folder_model_symlinks_created_count + folder_symlinks_created_count
pulled_models_info = " No Pull-Back "
if not DISABLE_PULLBACK_MODELS : # only show pulled models info, if "pull-back" is not disabled
pulled_models_info = f " Pulled( { pulled_model_files_count } ) "
message = f " Links managed: \n File( { file_model_symlinks_created_count } ), Folder( { folder_model_symlinks_created_count } ), Fixed( { broken_model_symlinks_count } ), { pulled_models_info } "
print ( f " \n \n Finished updating all model type symlinks into their defined app model type directories. " )
print ( message )
print ( f " \n For further customizatons following files were now generated: \n " )
print ( f " - README.md: provided help about the other 3 JSON files: \n " )
print ( f " - SHARED_MODEL_FOLDERS_FILE ' { SHARED_MODEL_FOLDERS_FILE } ' : \n provides examples of used ' model_type ' directory names for different models. \n " )
print ( f " - APP_INSTALL_DIRS_FILE ' { APP_INSTALL_DIRS_FILE } ' : \n provides examples of used ' app_name ' : ' app_install_dir ' mappings and is used together with the SHARED_MODEL_APP_MAP_FILE. \n " )
print ( f " - SHARED_MODEL_APP_MAP_FILE ' { SHARED_MODEL_APP_MAP_FILE } ' : \n provides examples of used ' model_type ' -> ' app_model_dir ' mappings. \n " )
print ( " Model symlinks updated. " )
return jsonify ( { ' status ' : ' success ' , ' message ' : message } ) # 'Symlinks (re-)created successfully.'
except Exception as e :
print ( f " ERROR in shared_models:update_model_symlinks() - Exception: \n { str ( e ) } " )
return jsonify ( { ' status ' : ' error ' , ' message ' : str ( e ) } )
# promote the README
print ( " To get started with all features of ' shared_models ' , consult the comprehensive README file " )
print ( ' \t " /app/tests/README-SHARED_MODELS.txt " \n It comes with a Test script and Test data. \n ' )
print ( " TESTDATA AND EXPLANATION OF MAPPING EVERYTHING YOU WANT \n " )
print ( ' In the folder " /app/tests " you find the following files: ' )
print ( ' \t - " README-SHARED_MODELS.txt " (this file) ' )
print ( ' \t - " populate_testdata.sh " (bash script to un-tar and expand all testdata into the " /workspace " folder) ' )
print ( ' \t - " testdata_shared_models_link.tar.gz " (Testcase #1, read below) ' )
print ( ' \t - " testdata_stable-diffusion-webui_pull.tar.gz " (Testcase #2, read below) ' )
print ( ' \t - " testdata_installed_apps_pull.tar.gz " (Testcase #3, read below) \n ' )