Initial Changes for localhost support and extended shared_models module

This commit is contained in:
lutzapps 2024-10-27 03:15:28 +07:00
parent 962a0cd3d4
commit f0c15b3315
31 changed files with 1906 additions and 34 deletions

View file

@ -0,0 +1,31 @@
**/.DS_Store
**/.dockerenv
**/filebrowser.db
**/docker-bake.hcl
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

View file

@ -0,0 +1,5 @@
**/.DS_Store
**/.dockerenv
**/__pycache__
**/.venv
**/.env

View file

@ -0,0 +1,28 @@
{
"configurations": [
{
"name": "Docker: Python - Flask",
"type": "docker",
"request": "launch",
"preLaunchTask": "docker-run: debug",
"python": {
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "/app"
}
],
"projectType": "flask"
}
},
{ // this whole section was added manually to add "justMyCode": false
"name": "Python: Debug Tests",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"purpose": ["debug-test"],
"console": "integratedTerminal",
"justMyCode": false
}
]
}

View file

@ -0,0 +1,72 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "docker-build",
"label": "docker-build",
"platform": "python",
"dockerBuild": {
"tag": "madiator2011/better-launcher:dev",
"dockerfile": "${workspaceFolder}/Dockerfile",
"context": "${workspaceFolder}",
"pull": true
}
},
{
"type": "docker-run",
"label": "docker-run: debug",
"dependsOn": [
"docker-build"
],
"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 the below folder before you run!
"localPath": "${userHome}/Projects/Docker/Madiator/workspace"
}
],
"ports": [
{
"containerPort": 7222, // main Flask app port "App-Manager"
"hostPort": 7222
},
{
"containerPort": 8181, // File-Browser
"hostPort": 8181
},
{
"containerPort": 7777, // VSCode-Server
"hostPort": 7777
}
]
},
"python": {
"args": [
"run",
// "--no-debugger", // disabled to support VSCode debugger
// "--no-reload", // disabled to support hot-reload
"--host",
"0.0.0.0",
"--port",
"7222"
],
"module": "flask"
}
}
]
}

View file

@ -1,13 +1,20 @@
# Use the specified base image
FROM madiator2011/better-base:cuda12.1 as base
# lutzapps - use uppercase "AS"
FROM madiator2011/better-base:cuda12.1 AS base
# lutzapps - prepare for local developement and debugging
# needed to change the ORDER of "apt-get commands" and move the "update-alternatives" for python3
# AFTER the "apt-get remove -y python3.10" cmd, OTHERWISE the symlink to python3
# is broken in the image and the VSCode debugger could not exec "python3" as CMD overwrite
# Install Python 3.11, set it as default, and remove Python 3.10
RUN apt-get update && \
apt-get install -y python3.11 python3.11-venv python3.11-dev python3.11-distutils aria2 git \
pv git rsync zstd libtcmalloc-minimal4 bc nginx ffmpeg && \
apt-get remove -y python3.10 python3.10-minimal libpython3.10-minimal libpython3.10-stdlib && \
update-alternatives --install /usr/bin/python python /usr/bin/python3.11 1 && \
update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 && \
apt-get remove -y python3.10 python3.10-minimal libpython3.10-minimal libpython3.10-stdlib && \
apt-get autoremove -y && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
@ -23,46 +30,64 @@ WORKDIR /app
# Copy the requirements file
COPY requirements.txt .
# Install the Python dependencies
# Install the Python dependencies (as "managed pip")
RUN python3.11 -mpip install --no-cache-dir -r requirements.txt
# Copy the application code
COPY . .
# lutzapps - only copy the "app" folder and not the root (".") to avoid cluttering the docker container with src-files
COPY app .
# Install File Browser
RUN curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash
# Set environment variables for production
# Set environment variables for developent/production
# overwrite this ENV in "tasks.json" docker run "env" section or overwrite in your ".env" file to "development"
ENV FLASK_ENV=production
# gevent monkey-patching is being used, enable gevent support in the debugger with GEVENT_SUPPORT=True
# add this ENV in "tasks.json" docker run "env" section or populate in your ".env" file
# ENV GEVENT_SUPPORT=True
# lutzapps - keep Python from generating .pyc files in the container
ENV PYTHONDONTWRITEBYTECODE=1
# Turns off buffering for easier container logging
ENV PYTHONUNBUFFERED=1
ENV APP_PATH=/app/app.py
# Expose the port Nginx will listen on
EXPOSE 7222
# lutzapps - moved the 4 static assets (3x PNG, 1x MP3) into the /app/static" folder for cleaner view
# lutzapps - added a "app/tests" folder with script and testdata and readme file
# lutzapps - grouped NGINX files in a sub-folder for cleaner view
# Copy the README.md
COPY README.md /usr/share/nginx/html/README.md
COPY nginx/README.md /usr/share/nginx/html/README.md
# NGINX configuration
COPY nginx.conf /etc/nginx/nginx.conf
COPY readme.html /usr/share/nginx/html/readme.html
# Create a directory for static files
RUN mkdir -p /app/static
# Copy the Poddy animation files to the static directory
COPY poddy.png /app/static/poddy.png
COPY mushroom.png /app/static/mushroom.png
COPY snake.png /app/static/snake.png
COPY poddy-song.mp3 /app/static/poddy-song.mp3
COPY nginx/nginx.conf /etc/nginx/nginx.conf
COPY nginx/readme.html /usr/share/nginx/html/readme.html
# Copy all necessary scripts
COPY --from=scripts start.sh /
COPY pre_start.sh /pre_start.sh
RUN chmod +x /pre_start.sh /start.sh
# --from=scripts is defined as a "shared" location in "docker-bake.hcl" in the "contexts" dict:
# scripts = "../../container-template"
# the local "start.sh" is (intentionally) empty
# to build all from *one* location, copy "start.sh" here into the project workspace folder first
# cp ../../container-template/scripts/start.sh start.sh
#COPY start.sh /
COPY pre_start.sh /
# lutzapps - add execution flags to added "/app/tests/populate_testdata.sh"
RUN chmod +x /pre_start.sh /start.sh /app/tests/populate_testdata.sh
# Copy the download_venv.sh script and make it executable
COPY download_venv.sh /app/download_venv.sh
RUN chmod +x /app/download_venv.sh
# CMD
# During debugging, this entry point will be overridden as "python3".
# For more information, please refer to https://aka.ms/vscode-docker-python-debug
CMD ["/start.sh"]

View file

@ -0,0 +1,62 @@
For local development of this image, you can run the image with 3 options:
- Docker Compose (production or Development)
- VS Code (F5 Debugging)
PREPARE ENV variables:
Both docker-compose YML files and the VS-Code "tasks.json" configuration file
pass a ".env" file into the container, to set User-specific ENV variables.
Edit the supplied "env.txt" template file with your values, and then rename it to ".env".
The ".env" file is in the ".gitignore" file to avoid unwanted secret-sharing with GitHub!
To build and run the image with DOCKER COMPOSE:
To run in "production":
Use the command "docker compose up":
That runs the container without debugger, but enables localhost and a workspace bind.
It uses the default "docker-compose.yml", which should be ADJUSTED to your workspace bind location.
This YML file binds all application ports (7222, 8181, 7777) all browsable from localhost,
and your SSH public key will be configured and be usable!
It uses 1 docker bind mount:
- "/workspace" can be bound to any location on your local machine (ADJUST the location in the YML file)
This option uses the default entry CMD "start.sh" as defined in the "Dockerfile".
To run in "development":
Use the command "docker compose -f docker-compose.debug.yml"
That runs the container with a python debugger (debugpy) attached, enables localhost browsing,
and binds the workspace from a local folder of your choice.
It uses 2 docker bind mounts:
- "/app" to mirror your app into the container (supports hot-reload) and debugging against your source files
- "/workspace" can be bound to any location on your local machine (ADJUST the location in the YML file)
This second debug YML configuration is configured with the same settings that are used for
VS-Code debugging with the VS-Code Docker Extension with Run -> Debug (F5).
BUT YOU NEED TO ATTACH THE DEBUGGER YOURSELF
Use the debugpy.wait_for_client() function to block program execution until the client is attached.
See more at https://github.com/microsoft/debugpy
It is much easier to build and run the image with VS-CODE (Run -> Debug F5):
The VS-Code Docker Extension uses 2 files in the hidden folder ".vscode":
- launch.json
- tasks.json
The "tasks.json" file is the file with most configuration settings
which basically mirrors to the "docker-compose.debug.yml"
ALL these 5 mention files (env.txt, docker-compose.yml, docker-compose.debug.yml,
launch.json, tasks.json, en.txt) are heavily commented.
NOTES
Both debugging options with "docker-compose.debug.yml", or VS-Code Docker Extenstions "tasks.json"
replace the CMD entry point of the Dockerfile to "python3", instead of "start.sh" !!!
That means that the apps on port 8181 (File-Browser) and 7777 (VSCode-Server) are
NOT available during debugging, neither will your SSH public key be configured and is not usable,
as these configuration things happen in "start.sh", but they are not needed during development.
Only the NGINX server and the Flask module will be started during debugging.

View file

@ -17,8 +17,44 @@ from utils.filebrowser_utils import configure_filebrowser, start_filebrowser, st
from utils.app_utils import (
run_app, update_process_status, check_app_directories, get_app_status,
force_kill_process_by_name, update_webui_user_sh, save_install_status,
get_install_status, download_and_unpack_venv, fix_custom_nodes, is_process_running, install_app, update_model_symlinks
get_install_status, download_and_unpack_venv, fix_custom_nodes, is_process_running, install_app #, update_model_symlinks
)
# lutzapps - CHANGE #1
LOCAL_DEBUG = os.environ.get('LOCAL_DEBUG', 'False') # support local browsing for development/debugging
# use the new "utils.shared_models" module for app model sharing
from utils.shared_models import (
update_model_symlinks, # main WORKER function (file/folder symlinks, Fix/remove broken symlinks, pull back local app models into shared)
SHARED_MODELS_DIR, SHARED_MODEL_FOLDERS, SHARED_MODEL_FOLDERS_FILE, ensure_shared_models_folders,
APP_INSTALL_DIRS, APP_INSTALL_DIRS_FILE, init_app_install_dirs, # APP_INSTALL_DIRS dict/file/function
MAP_APPS, sync_with_app_configs_install_dirs, # internal MAP_APPS dict and sync function
SHARED_MODEL_APP_MAP, SHARED_MODEL_APP_MAP_FILE, init_shared_model_app_map, # SHARED_MODEL_APP_MAP dict/file/function
write_dict_to_jsonfile, read_dict_from_jsonfile, PrettyDICT # JSON helper functions
)
# the "update_model_symlinks()" function replaces the app.py function with the same same
# and redirects to same function name "update_model_symlinks()" in the new "utils.shared_models" module
#
# this function does ALL the link management, including deleting "stale" symlinks,
# so the "recreate_symlinks()" function will be also re-routed to the
# "utils.shared_models.update_model_symlinks()" function (see CHANGE #3a and CHANGE #3b)
# the "ensure_shared_models_folders()" function will be called from app.py::create_shared_folders(),
# and replaces this function (see CHANGE #3)
# the "init_app_install_dirs() function initializes the
# global module 'APP_INSTALL_DIRS' dict: { 'app_name': 'app_installdir' }
# which does a default mapping from app code or (if exists) from external JSON 'APP_INSTALL_DIRS_FILE' file
# NOTE: this APP_INSTALL_DIRS dict is temporary synced with the 'app_configs' dict (see next)
# the "sync_with_app_configs_install_dirs() function syncs the 'APP_INSTALL_DIRS' dict's 'app_installdir' entries
# from the 'app_configs' dict's 'app_path' entries and uses the MAP_APPS dict for this task
# NOTE: this syncing is a temporary solution, and needs to be better integrated later
# 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
from utils.websocket_utils import send_websocket_message, active_websockets
from utils.app_configs import get_app_configs, add_app_config, remove_app_config, app_configs
from utils.model_utils import download_model, check_civitai_url, check_huggingface_url, SHARED_MODELS_DIR, format_size
@ -91,6 +127,11 @@ def index():
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'),
settings=settings,
current_auth_method=current_auth_method,
ssh_password=ssh_password,
@ -297,7 +338,20 @@ def remove_existing_app_config(app_name):
return jsonify({'status': 'success', 'message': f'App {app_name} removed successfully'})
return jsonify({'status': 'error', 'message': f'App {app_name} not found'})
# unused function
def obsolate_update_model_symlinks():
# lutzapps - CHANGE #3 - use the new "shared_models" module for app model sharing
# remove this whole now unused function
return "replaced by utils.shared_models.update_model_symlinks()"
# modified function
def setup_shared_models():
# lutzapps - CHANGE #4 - use the new "shared_models" module for app model sharing
jsonResult = update_model_symlinks()
return SHARED_MODELS_DIR # shared_models_dir is now owned and managed by the "shared_models" utils module
# remove below unused code
shared_models_dir = '/workspace/shared_models'
model_types = ['Stable-diffusion', 'VAE', 'Lora', 'ESRGAN']
@ -326,7 +380,12 @@ def setup_shared_models():
return shared_models_dir
def update_model_symlinks():
# unused function
def obsolate_update_model_symlinks():
# lutzapps - CHANGE #5 - use the new "shared_models" module for app model sharing
# remove this whole now unused function
return "replaced by utils.shared_models.update_model_symlinks()"
shared_models_dir = '/workspace/shared_models'
apps = {
'stable-diffusion-webui': '/workspace/stable-diffusion-webui/models',
@ -375,7 +434,12 @@ def start_symlink_update_thread():
thread = threading.Thread(target=update_symlinks_periodically, daemon=True)
thread.start()
def recreate_symlinks():
# unused function
def obsolate_recreate_symlinks():
# lutzapps - CHANGE #6 - use the new "shared_models" module for app model sharing
# remove this whole now unused function
return "replaced by utils.shared_models.update_model_symlinks()"
shared_models_dir = '/workspace/shared_models'
apps = {
'stable-diffusion-webui': '/workspace/stable-diffusion-webui/models',
@ -421,16 +485,29 @@ def recreate_symlinks():
return "Symlinks recreated successfully."
# modified function
@app.route('/recreate_symlinks', methods=['POST'])
def recreate_symlinks_route():
# lutzapps - CHANGE #7 - use the new "shared_models" module for app model sharing
jsonResult = update_model_symlinks()
return jsonResult
# remove below unused code
try:
message = recreate_symlinks()
return jsonify({'status': 'success', 'message': message})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)})
# modified function
@app.route('/create_shared_folders', methods=['POST'])
def create_shared_folders():
# lutzapps - CHANGE #8 - use the new "shared_models" module for app model sharing
jsonResult = ensure_shared_models_folders()
return jsonResult
# remove below unused code
try:
shared_models_dir = '/workspace/shared_models'
model_types = ['Stable-diffusion', 'Lora', 'embeddings', 'VAE', 'hypernetworks', 'aesthetic_embeddings', 'controlnet', 'ESRGAN']
@ -487,6 +564,35 @@ def get_civitai_token_route():
token = load_civitai_token()
return jsonify({'token': token})
# lutzapps - CHANGE #9 - return model_types to populate the Download manager Select Option
# new function to support the "Model Downloader" with the 'SHARED_MODEL_FOLDERS' dictionary
@app.route('/get_model_types', methods=['GET'])
def get_model_types_route():
model_types_dict = {}
# check if the SHARED_MODELS_DIR exists at the "/workspace" location!
# that only happens AFTER the the user clicked the "Create Shared Folders" button
# on the "Settings" Tab of the app's WebUI!
# to reload existing SHARED_MODEL_FOLDERS into the select options dropdown list,
# we send a WebSockets message to "index.html"
if not os.path.exists(SHARED_MODELS_DIR):
# return an empty model_types_dict, so the "Download Manager" does NOT get
# the already in-memory SHARED_MODEL_FOLDERS code-generated default dict
# BEFORE the workspace folders in SHARED_MODELS_DIR exists
return model_types_dict
i = 0
for model_type, model_type_description in SHARED_MODEL_FOLDERS.items():
model_types_dict[i] = {
'modelfolder': model_type,
'desc': model_type_description
}
i = i + 1
return model_types_dict
@app.route('/download_model', methods=['POST'])
def download_model_route():
url = request.json.get('url')

View file

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View file

Before

Width:  |  Height:  |  Size: 328 KiB

After

Width:  |  Height:  |  Size: 328 KiB

View file

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View file

@ -1211,11 +1211,11 @@
<div class="model-downloader">
<input type="text" id="modelUrl" placeholder="Enter Civitai or Hugging Face URL">
<input type="text" id="modelName" placeholder="Enter model name (optional)">
<select id="modelType">
<option value="Stable-diffusion">Checkpoint (Stable Diffusion)</option>
<option value="Lora">LoRA</option>
<option value="VAE">VAE</option>
<option value="ESRGAN">Upscaler</option>
<select id="modelType"><!-- lutzapps - Change #1 - remove default modelType select option values -->
<!-- <option value="Stable-diffusion">Checkpoint (Stable Diffusion)</option> -->
<!-- <option value="Lora">LoRA</option> -->
<!-- <option value="VAE">VAE</option> -->
<!-- <option value="ESRGAN">Upscaler</option> -->
</select>
<input type="password" id="civitaiToken" placeholder="Civitai API Token">
<input type="text" id="hfToken" placeholder="Hugging Face API Token (optional)">
@ -1302,7 +1302,23 @@
let currentLogAppName = null;
const podId = '{{ pod_id }}';
const WS_PORT = 7222; // This is the Nginx port
const WS_URL = `wss://${podId}-${WS_PORT}.proxy.runpod.net/ws`;
// *** 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}`);
}
let socket;
let reconnectAttempts = 0;
@ -1446,7 +1462,14 @@
}
function openApp(appKey, port) {
const url = `https://${podId}-${port}.proxy.runpod.net/`;
// *** 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');
}
@ -1510,6 +1533,41 @@
updateStatus();
setInterval(updateStatus, 5000);
setInterval(updateLogs, 1000);
initModelTypes(); // lutzapps - Change #4a - initialize the ModelTypes for the "Model Downloader"
}
// lutzapps - Change #4b - populate modeltype select options from shared_models
async function initModelTypes() {
const modelTypeSelect = document.getElementById('modelType');
// get the data from the Server
const response = await fetch('/get_model_types');
const result = await response.json();
//alert(JSON.stringify(result)); // show the JSON-String
var model_types = result; // get the JSON-Object
var count = Object.keys(model_types).length; // #18 when using the default SHARED_MODEL_FOLDERS dict
// the "/get_model_types" app.get_model_types_route() function checks
// if the SHARED_MODELS_DIR shared files already exists at the "/workspace" location.
// that only happens AFTER the the user clicked the "Create Shared Folders" button
// on the "Settings" Tab of the app's WebUI.
// it will return an empty model_types_dict, so the "Download Manager" does NOT get
// the already in-memory SHARED_MODEL_FOLDERS code-generated default dict
// BEFORE the workspace folders in SHARED_MODELS_DIR exists!
//
// when SHARED_MODELS_DIR exists (or updates), this function will be called via a Socket Message
// to "refresh" its content automatically
for (i = 0 ; i < count; i += 1) {
modelTypeOption = document.createElement('option');
modelTypeOption.setAttribute('value', model_types[String(i)]['modelfolder']);
modelTypeOption.appendChild(document.createTextNode(model_types[String(i)]['desc']));
modelTypeSelect.appendChild(modelTypeOption);
}
}
async function downloadModel() {
@ -1915,7 +1973,15 @@
function openFileBrowser() {
const podId = '{{ pod_id }}';
const url = `https://${podId}-7222.proxy.runpod.net/fileapp/`;
// *** 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');
}
@ -2015,6 +2081,9 @@
updateModelDownloadProgress(data.data);
} else if (data.type === 'status_update') {
updateAppStatus(data.data);
// lutzapps - Change #6 - int the "Model Downloader's" ModelTypes select dropdown list
} else if (data.type === 'init_model_downloader_model_types') {
initModelTypes(data.data);
}
// Handle other message types as needed
} catch (error) {

View file

@ -0,0 +1,469 @@
TESTDATA AND EXPLANATION OF MAPPING EVERYTHING YOU WANT
In the folder "/app/tests" you find the following files:
/app/tests/
- "README-SHARED_MODELS.txt" (this file)
- "populate_testdata.sh" (bash script to un-tar and expand all testdata into the "/workspace" folder)
- "testdata_shared_models_link.tar.gz" (Testcase #1, read below)
- "testdata_stable-diffusion-webui_pull.tar.gz" (Testcase #2, read below)
- "testdata_installed_apps_pull.tar.gz" (Testcase #3, read below)
CREATE TESTDATA (once done already):
cd /workspace
# For Testcase #1 - create testdata in "shared_models" folder with dummy models for most model_types:
$ tar -czf testdata_shared_models_link.tar.gz shared_models
# For Testcase #2 - create testdata with SD-Models for A1111 to be pulled back into "shared_models" and linked back:
$ tar -czf testdata_stable-diffusion-webui_pull.tar.gz stable-diffusion-webui
# For Testcase #3 -create testdata with all possible "Apps" installed into your "/workspace"
$ tar -czf /app/tests/testdata_installed_apps_pull.tar.gz Apps
USE TESTDATA:
# BEFORE(!) you run this script, read the readme ;-)
/app/tests/populate_testdata.sh::
# use these 3 test cases and extract/merge them accordingly into your workspace, bur READ before you mess you up too much!!
tar -xzf /app/tests/testdata_shared_models_link.tar.gz /workspace
tar -xzf /app/tests/testdata_stable-diffusion-webui_pull.tar.gz /workspace
tar -xzf /app/tests/testdata_installed_apps_pull.tar.gz /workspace
Testcase #1:
When you expand "./testdata_shared_models_link.tar.gz" into the "/workspace" folder, you get:
$ tree shared_models
shared_models
├── LLM
│   └── Meta-Llama-3.1-8B
│   ├── llm-Llama-modelfile1.txt
│   ├── llm-Llama-modelfile2.txt
│   └── llm-Llama-modelfile3.txt
├── ckpt
│   ├── ckpt-model1.txt
│   └── ckpt-model2.txt
├── clip
│   └── clip-model1.txt
├── controlnet
│   └── controlnet-model1.txt
├── embeddings
│   ├── embedding-model1.txt
│   └── embedding-model2.txt
├── hypernetworks
│   └── hypernetworks-model1.txt
├── insightface
│   └── insightface-model1.txt
├── ipadapters
│   ├── ipadapter-model1.txt
│   └── xlabs
│   └── xlabs-ipadapter-model1.txt
├── loras
│   ├── flux
│   │   └── flux-lora-model1.txt
│   ├── lora-SD-model1.txt
│   ├── lora-SD-model2.txt
│   ├── lora-SD-model3.txt
│   ├── lora-SD-model4.txt
│   ├── lora-SD-model5.txt
│   ├── lora-model1.txt
│   ├── lora-model2.txt
│   └── xlabs
│   └── xlabs-lora-model1.txt
├── reactor
│   ├── faces
│   │   └── reactor-faces-model1.txt
│   └── reactor-model1.txt
├── unet
│   ├── unet-model1.txt
│   └── unet-model2.txt
├── upscale_models
│   └── esrgan-model1.txt
├── vae
│   └── vae-model1.txt
└── vae-approx
└── vae-apporox-model1.txt
20 directories, 29 files
All these "*.txt" files "simulate" model files of a specific category (model type).
When you have this test data and you click the "Recreate Symlinks" button on the "Settings" Tab, all these models will be shared with all "installed" apps, like:
A1111: /workspace/stable-diffusion-webui
Forge: /workspace/stable-diffusion-webui-forge
ComfyUI: /workspace/ComfyUI
Kohya_ss: /workspace/Kohya_ss
CUSTOM1: /workspace/joy-caption-batch
To "simulate" the installed app, you just need to create one or all of these folders manually, as empty folders. Maybe try it one-by-one, like you would do "in-real-life".
After there is at least ONE app installed, you can test the model sharing with the above mentioned Button "Recreate Symlinks".
All of these 29 models should be shared into all "installed" apps.
When you "add" a second app, also this new app will get all these models shared into its model folders, which can be differently named.
Some model types (e.g. UNET) have a separate model folder from Checkpoints in ComfyUI, but in A1111/Forge, these 2 model types will be merged ("flattened") in one "Stable-Diffusion" model folder. See later in the third MAP shown here.
The same goes in the other direction.
LoRA models which are "organized" in subfolders like "flux" or "slabs" in shared models folder, or in ComfyUI will again be flattened for app like A1111/Forge into the only ONE "Lora" folder.
Pay attention to the details, as the Folder is called "Lora" (no "s" and capital "L" for A111/Forge), but is called "Loras" (with ending "s" and all lower-case).
You can even map/share LLM models, which mostly come installed as a folder with many files needed for one LLM. For these models, FOLDER symlinks will be created instead or regular file symlinks which are sufficient for most model files of many model types.
Some model types, like "Embeddings" are managed differently for A1111/Forge (outside its regular model folder), which is not the case for ComfyUI. All this can be tested.
You can also test to delete a "shared model" in the "shared_models directory, and all its "symlinked" copies should be also automatically removed.
Testcase #2:
In the second testdata TAR archive, you have some SD-models which simulate the installation of the model files once for ONE app, in this test case only for A1111.
The "./testdata_stable-diffusion-webui_pull.tar.gz" is easier to handle than the one for Testcase #3, as it installs directly into the "original" App install location.
$ tree stable-diffusion-webui
stable-diffusion-webui
├── _add
│   ├── lora-SD-model2.txt
│   ├── lora-SD-model3.txt
│   ├── lora-SD-model4.txt
│   └── lora-SD-model5.txt
└── models
└── Lora
└── lora-SD-model1.txt
4 directories, 5 files
Testcase #3:
In this test case you also have other apps installed already, but the principle is the same, just a little bit more careful folder management.
The "./testdata_installed_apps_pull.tar.gz.tar.gz" extracts into an "Apps" folder.
All folders in this extracted "Apps" folder should be copied into the "/workspace" folder, to simulate an installed A1111, Forge, ComfyUI and Kohya_ss. Make sure that at the end you NOT see the extracted "Apps" folder anymore, as you only used its SUB-FOLDERS to copy/move them into "/workspace" and replace/merge existing folder.
$ tree Apps
Apps
├── ComfyUI
├── Kohya_ss
├── _add
│   ├── lora-SD-model2.txt
│   ├── lora-SD-model3.txt
│   ├── lora-SD-model4.txt
│   └── lora-SD-model5.txt
├── joy-caption-batch
├── stable-diffusion-webui
│   └── models
│   └── Lora
│   └── lora-SD-model1.txt
└── stable-diffusion-webui-forge
9 directories, 5 files
This test cases #2 and #3 start with one SD LoRA model "lora-SD-model1.txt" installed only for "stable-diffusion-webui" (A1111) in its "Lora" model folder.
Wenn you have this test data installed, and you click the "Recreate Symlinks" button of the "better-ai-laucncher" template, then this "locally" (one-app-only) installed LoRA model will be found and "pulled" back into the "shared_models/loras" folder, and "re-shared" back to the pulled location in A1111.
But it will then also be shared to all other "installed" apps, like ComfyUI, Forge. But not into Kohya, as there is no "mapping rule" defined for Kohya for LoRA models.
The only "mapping rule" for Kohya which is defined, is to get all "ckpt" (Checkpoint) model files and all UNET model files shared from the corresponding "shared_models" subfolders into its /models folder (see later in the 3rd MAP below).
In the testdata "Apps" folder you also find an "_add" folder, with 4 more SD-Models to play around with the App Sharing/Syncing framework. Put the in any local app model folder and watch what happens to them and where they the can be seen/used from other apps. You either wait a fewMinutes to let this happen automatically (every 5 Minutes), or you press the "Recreate Symlinks" button at any time to kick this off.
You can also test to see what happens, when you DELETE a model file from the shared_models sub-folders, and that all its symlinks shared to all apps will also automatically be removed, so no broken links will be left behind.
When you delete a symlink in an app model folder, only the local app "looses" the model (it is just only a link to the original shared model), so no worries here. Such locally removed symlinks however will be re-created again automatically.
BUT THAT IS ONLY THE BEGINNING.
All this logic described here is controlled via 3 (three) "MAP" dictionary JSON files, which can be found also in the "/workspace/shared_models" folder, after you click the "Create Shared Folders" button on the "Settings" Tab. They will be auto-generated, if missing, or otherwise "used-as-is" :
1.) The "SHARED_MODEL_FOLDERS" map found as "SHARED_MODEL_FOLDERS_FILE"
"/workspace/shared_models/_shared_model_folders.json":
{
# "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"
}
This is the "SHARED_MODEL_FOLDERS" map for the "shared_models" sub-folders, which are used by the App Model Sharing, and this is also the dictionary which is used by the "Model Downloader" Dropdown Listbox, which can be found on the "Models" Tab of the WebUI.
The idea is to make it easy for people to download their models from "Huggingface" or "CivitAI" directly into the right model type subfolder of the "/workspace/shared_models" main folder.
This "SHARED_MODEL_FOLDERS" map also is used when you click the "Create Shared Folders" button.
NOTE: pay special attention to the examples with the "loras" folder. There is one regular model type for "loras", and 2 "grouping" model types, "loras/flux" and "loras/xlabs".
We come back to "grouping" mapping rules later, when we discuss the 3rd map.
Feel free to add/remove/edit/rename items here, alls should be re-created automatically. Just nothing from renamed folders will be deleted.
NOTE: if you change something in this map file, you need to "read" it into the App via the "Create Shared Folders" button on the "Settings" Tab of the WebUI.
If new folders are found in the map, they will be created, but nothing you have already created before will be deleted or renamed automatically, so be careful not generating two folders for the same model type, or move the models manually into the renamed folder.
IMPORTANT: Also be aware that when you add or change/rename folder names here, you need to also add or change/rename these folder names in the third "SHARED_MODEL_APP_MAP" explained below!!!
2.) The "APP_INSTALL_DIRS" map found as "APP_INSTALL_DIRS_FILE"
"/workspace/shared_models/_app_install_dirs.json":
{
# "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"
}
This is the "APP_INSTALL_DIRS" map for the app install dirs within the "/workspace", and as you see, it also supports "CUSTOM" apps to be installed and participating at the model sharing.
This dictionary is "synced" with the main apps "app_configs" dictionary, so the installation folders are the same, and this should NOT be changed. What you can change in this MAP is to add "CUSTOM" apps, like "CUSTOM1" here e.g. to re-use the Llama LLM model which is centrally installed in "shared_models" under the LLM folder to be "shared" between ComfyUI and "Joy Caption Batch" tool, which is nice to generate your "Caption" files for your LoRA Training files with "Kohya_ss" for example.
3.) The "SHARED_MODEL_APP_MAP" map found as "SHARED_MODEL_APP_MAP_FILE"
"/workspace/shared_models/_shared_model_app_map.json":
{
"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
}
}
This third and last "SHARED_MODEL_APP_MAP" "connects" the "SHARED_MODEL_FOLDERS" map with the "APP_INSTALL_DIRS" map, to have a very flexible "Mapping" between all apps.
You can custom define all folder/directory layouts and namings of model types, which you can think of.
Add new mappings to your likings.
And if you don't need some of these mappings, then change them, or delete them.
This is sample data to give you a head start what you can do.
NOTE: as already introduced before, with the 3 "loras" model type folders, here we now see the 2 grouping model types "/loras/flux" and "/loras/xlabs" now applied in a "grouping" map rule.
"Grouping" of specific model.
E.g. LoRAs into separate sub-folders for LoRAs (e.g. "flux", "xlabs") for easier "filtering" in ComfyUI. Look this MAP and the testdata for LoRA samples of that feature.
It shows it for 2 LoRA sub-folders as just mentioned. These 2 "grouping" map rules also show how to "flatten" these sub-folders into only one "Lora" model folder for A1111/Forge, all of them go flat into their "Lora" folder, as these are apps, that do NOT support sub-folders per model type, and need therfore to have these "flux" and "xlabs" LoRAs "flattended" to see and consume them.
Try to add your own "grouping" map rule, or delete "grouping" map rules you not need.
Otherwise this "SHARED_MODEL_APP_MAP" should be self-explanatory, except for the last part with the FOLDER sharing syntax, as shown here and already mentioned LLM "Meta-Llama-3.1-8B", which will be used here, to show how an LLM model as "Meta-Llama-3.1-8B" can be shared between the app "ComfyUI" and a "CUSTOM1" defined app "joy-caption-batch".
FOLDER SHARING, e.g. LLM folder-based models:
_app_install.dirs.json:
{
...
"Kohya_ss": "/workspace/Kohya_ss",
"CUSTOM1": "/workspace/joy-caption-batch"
}
_shared_model_folders.json:
{
...
"LLM": "LLM (aka Large-Language Model) is folder mapped (1 folder per model), append '/*' in the map",
}
To define a "folder" map rule, the "rule" must be an EXISTING shared foldername trailing with "/*",
e.g.
_shared_model_app_map.json:
{
...
"LLM/Meta-Llama-3.1-8B/*": {
"ComfyUI": "/models/LLM/Meta-Llama-3.1-8B/*",
"CUSTOM1": "/model/*"
}
}
This difference in the syntax with a trailing "/*" shows the shared_models Framework, that you NOT want the model files IN the folder to be shared one-by-one (which would not work anyway for a LLM model), but that you want to share the whole FOLDER, so EVERYTHING within the folder is also automatically shared.
Just append a "/*" to the MAP rule of the physical folder name "LLM/Meta-Llama-3.1-8B", and it will understand, that you want a FOLDER symlink from the folder path with the "/*" removed.
All pathes are relative to "/workspace/shared_models".
This will "trigger" a "folder" symlink to all defined target app folder mapped folders.
The target foldernames (defined with or without the trailing "/*") must be a NON-EXISTING app foldername, which will be the target folder symlink from the shared folder source.
Here are the detailes of this folder symlink for "ComfyUI":
$ cd ComfyUI/models/LLM
$ ls -la
drwxr-xr-x 3 root root 96 Oct 25 11:45 .
drwxr-xr-x 18 root root 576 Oct 25 11:45 ..
lrwxr-xr-x 1 root root 46 Oct 25 11:45 Meta-Llama-3.1-8B -> /workspace/shared_models/LLM/Meta-Llama-3.1-8B
NOTE: pay special attention to the second folder map rule for the "CUSTOM1" app "joy-cation-batch". The target mapped folder "model" has a different name from the source folder "LLM/Meta-Llama-3.1-8B/", and the 3 sample LLM model files all go directly into the linked "/model" folder.
Here are the detailes of this folder symlink for "CUSTOM1" (joy-caption-batch):
$ cd joy-caption-batch
$ ls -la
drwxr-xr-x 3 root root 96 Oct 25 11:53 .
drwxr-xr-x 10 root root 320 Oct 25 11:45 ..
lrwxr-xr-x 1 root root 46 Oct 25 11:53 model -> /workspace/shared_models/LLM/Meta-Llama-3.1-8B
$ cd model
$ ls -la
drwxr-xr-x 6 root root 192 Oct 23 10:22 .
drwxr-xr-x 5 root root 160 Oct 23 10:27 ..
-rwx------ 1 root root 51 Oct 18 11:29 llm-Llama-modelfile1.txt
-rwx------ 1 root root 51 Oct 18 11:29 llm-Llama-modelfile2.txt
-rwx------ 1 root root 51 Oct 18 11:29 llm-Llama-modelfile3.txt
Folder Sharing rules is an advanced technique, and they can only be manually added, editing the "SHARED_MODEL_APP_MAP" file for such rules, as shown above.
While all new downloaded "single" model files of a model type will be automatically shared into all app model folders, "folder models" (as typically found for LLM models), need to be added to this "SHARED_MODELS_MAP" JSON file manually to be shared, as it is shown in this example.
You could also use this folder mappings for "custom" LoRA sub-categories in ComfyUI "per folder", instead of sub-folders with separate file symlinks, but I not want to go deeper here.
You can try it, nothing bad will happen.
If you delete one of these three MAP JSON files, they will be re-generated with its shown "default" content.
But when you edit/change these 3 MAP JSON files, they will be used with your changes/addings every time you use this "/workspace" volume.
That's it for the FULL version of "shared_models" sharing between apps, and how you can easily test this before using the "real data" ;-)
Regards,
lutzapps

View file

@ -0,0 +1,12 @@
#!/bin/bash
# please use the "./readme-testdata.txt" before you extract these TARs!!!
# Testcase #1
tar -xzf /app/tests/testdata_shared_models_link.tar.gz /workspace
# Testcase #2
tar -xzf /app/tests/testdata_stable-diffusion-webui_pull.tar.gz /workspace
# Testcase #3
tar -xzf /app/tests/testdata_installed_apps_pull.tar.gz /workspace

View file

@ -300,7 +300,12 @@ def install_app(app_name, app_configs, send_websocket_message):
else:
return False, f"Unknown app: {app_name}"
def update_model_symlinks():
# unused function
def onsolate_update_model_symlinks():
# lutzapps - CHANGE #7 - use the new "shared_models" module for app model sharing
# remove this whole now unused function
return "replaced by utils.shared_models.update_model_symlinks()"
shared_models_dir = '/workspace/shared_models'
apps = {
'stable-diffusion-webui': '/workspace/stable-diffusion-webui/models',

View file

@ -0,0 +1,915 @@
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)
### shared_models-v0.7 by ViennaFlying, Oct 25th 2024 ###
### dev-my-v0.3
# 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}'\nException: {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}'\nException: {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"\nThe SHARED_MODELS_DIR '{SHARED_MODELS_DIR}' is not found!\nCreate 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"\nExisting '{dict_description}' found and read from file '{dict_filepath}'\nThe 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"\nUsing {'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}'\nException: {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:
send_websocket_message('init_model_downloader_model_types', {
'stage': 'Refresh',
'message': 'New ModelTypes are available'
})
return
### "inline"-main() ###
# init the SHARED_MODEL_FOLDERS
init_shared_models_folders(False) # dont send a WS-Message for "Model Downloader" at module init to init/refresh the model_type list
# ----------
# helper function called from "app.py" via WebUI
# 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'
# from app code or from external JSON 'SHARED_MODEL_FOLDERS_FILE' file
init_shared_models_folders()
print(f"(re-)creating 'shared_models' model type sub-folders for Apps and the 'Model Downloader' in folder '{SHARED_MODELS_DIR}':")
# create the shared_models directory, if it doesn't exist
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)
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("\nThese 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")
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"\tSyncing '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"\nUsing 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"\tremoved 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"\tpulled-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"\tre-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\nConsider 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"\tprocessing 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"\tthis is a sub-folder which should be mapped with a 'grouping' rule,\n\te.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"\tcreated 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
def update_model_symlinks():
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}'\nfor 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,\nfor 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)\nyou 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"\tcreated 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:\nFile({file_model_symlinks_created_count}), Folder({folder_model_symlinks_created_count}), Fixed({broken_model_symlinks_count}), {pulled_models_info}"
print(f"\n\nFinished updating all model type symlinks into their defined app model type directories.")
print(message)
print(f"\nFor 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}':\nprovides examples of used 'model_type' directory names for different models.\n")
print(f"- APP_INSTALL_DIRS_FILE '{APP_INSTALL_DIRS_FILE}':\nprovides 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}':\nprovides 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"\nIt 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')

View file

@ -0,0 +1,23 @@
services:
mydev:
image: madiator2011/better-launcher:dev
build:
context: .
dockerfile: ./Dockerfile
command: ["sh", "-c", "pip install debugpy -t /tmp && python /tmp/debugpy --listen 0.0.0.0:5678 -m flask run --host 0.0.0.0 --port 7222"]
ports:
- 5678:5678 # (random) port for debuggy (adjust together in above "command")
- 7222:7222 # main Flask app port better-launcher "App-Manager"
- 8181:8181 # File-Browser
- 7777:7777 # VSCode-Server
env_file:
- .env # pass additional env-vars (hf_token, civitai token, ssh public-key) from ".env" file to container
environment:
- 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:
- ./app:/app:rw
- ${HOME}/Projects/Docker/madiator:/workspace:rw # TODO: create the below folder before you run!

View file

@ -0,0 +1,16 @@
services:
mydev:
image: madiator2011/better-launcher:dev
build:
context: .
dockerfile: ./Dockerfile
env_file:
- .env # pass additional env-vars (hf_token, civitai token, ssh public-key) from ".env" file to container
environment:
- LOCAL_DEBUG=True # change app to localhost Urls and local Websockets (unsecured)
volumes:
- ${HOME}/Projects/Docker/madiator:/workspace:rw # # TODO: create the below folder before you run!
ports:
- 7222:7222 # main Flask app port better-launcher "App-Manager"
- 8181:8181 # File-Browser
- 7777:7777 # VSCode-Server

View file

@ -0,0 +1,34 @@
### for local debugging fill-in/update YOUR env values and secrets
### and rename this file as ".env" (hidden file with NO extension)
### the ".env" file is also added to the ".dockerignore" and ".gitignore" files,
### to NOT pass them to the container (".dockerignore") or to GitHub (".gitignore")
###
### this ".env" file is then "passed" into the debugging container via ".vscode/tasks.json"
### in the "dockerRun" section:
#
# "dockerRun": {
# "envFiles": ["${workspaceFolder}/.env"], // pass additional env-vars from ".env" file to container
### Build Vars ###
IMAGE_BASE=madiator2011/better-launcher
IMAGE_TAG=dev
### APP specific Vars ###
DISABLE_PULLBACK_MODELS=False
# the default is, that app model files, which are found locally (in only one app),
# get automatically "pulled-back" into the '/workspace/shared_models' folder.
# From there they will be re-linked back not only to their own "pulled-back" model-type folder,
# but also will be linked back into all other corresponding app model-type folders.
# So the "pulled-back" model is automatically shared to all installed apps.
#
# if you NOT want this behaviour, then set DISABLE_PULLBACK_MODELS=True
### USER specific Vars and Secrets (Tokens) - TODO: adjust this for your personal settings ###
PUBLIC_KEY=ssh-ed25519 XXX...XXX usermail@domain.com
HF_TOKEN=hf_XXX...XXX
CIVITAI_API_TOKEN=XXX.XXX
### RUNPOD specific Vars ###
RUNPOD_PUBLIC_IP=127.0.0.1
RUNPOD_TCP_PORT_22=22