diff --git a/official-templates/better-ai-launcher/.dockerignore b/official-templates/better-ai-launcher/.dockerignore
new file mode 100644
index 0000000..566bea3
--- /dev/null
+++ b/official-templates/better-ai-launcher/.dockerignore
@@ -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
diff --git a/official-templates/better-ai-launcher/.gitignore b/official-templates/better-ai-launcher/.gitignore
new file mode 100644
index 0000000..eef3538
--- /dev/null
+++ b/official-templates/better-ai-launcher/.gitignore
@@ -0,0 +1,5 @@
+**/.DS_Store
+**/.dockerenv
+**/__pycache__
+**/.venv
+**/.env
diff --git a/official-templates/better-ai-launcher/.vscode/launch.json b/official-templates/better-ai-launcher/.vscode/launch.json
new file mode 100644
index 0000000..101b4d6
--- /dev/null
+++ b/official-templates/better-ai-launcher/.vscode/launch.json
@@ -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
+ }
+ ]
+}
\ No newline at end of file
diff --git a/official-templates/better-ai-launcher/.vscode/tasks.json b/official-templates/better-ai-launcher/.vscode/tasks.json
new file mode 100644
index 0000000..f07dd81
--- /dev/null
+++ b/official-templates/better-ai-launcher/.vscode/tasks.json
@@ -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"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/official-templates/better-ai-launcher/Dockerfile b/official-templates/better-ai-launcher/Dockerfile
index 4fedb34..ae3cdab 100644
--- a/official-templates/better-ai-launcher/Dockerfile
+++ b/official-templates/better-ai-launcher/Dockerfile
@@ -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"]
\ No newline at end of file
diff --git a/official-templates/better-ai-launcher/README-Development.txt b/official-templates/better-ai-launcher/README-Development.txt
new file mode 100644
index 0000000..523717f
--- /dev/null
+++ b/official-templates/better-ai-launcher/README-Development.txt
@@ -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.
\ No newline at end of file
diff --git a/official-templates/better-ai-launcher/app.py b/official-templates/better-ai-launcher/app/app.py
similarity index 81%
rename from official-templates/better-ai-launcher/app.py
rename to official-templates/better-ai-launcher/app/app.py
index 49c9679..0cc72de 100644
--- a/official-templates/better-ai-launcher/app.py
+++ b/official-templates/better-ai-launcher/app/app.py
@@ -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',
@@ -367,7 +426,7 @@ def update_model_symlinks():
print("Model symlinks updated.")
def update_symlinks_periodically():
- while True:
+ while True:
update_model_symlinks()
time.sleep(300) # Check every 5 minutes
@@ -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')
diff --git a/official-templates/better-ai-launcher/gunicorn.conf.py b/official-templates/better-ai-launcher/app/gunicorn.conf.py
similarity index 100%
rename from official-templates/better-ai-launcher/gunicorn.conf.py
rename to official-templates/better-ai-launcher/app/gunicorn.conf.py
diff --git a/official-templates/better-ai-launcher/mushroom.png b/official-templates/better-ai-launcher/app/static/mushroom.png
similarity index 100%
rename from official-templates/better-ai-launcher/mushroom.png
rename to official-templates/better-ai-launcher/app/static/mushroom.png
diff --git a/official-templates/better-ai-launcher/poddy-song.mp3 b/official-templates/better-ai-launcher/app/static/poddy-song.mp3
similarity index 100%
rename from official-templates/better-ai-launcher/poddy-song.mp3
rename to official-templates/better-ai-launcher/app/static/poddy-song.mp3
diff --git a/official-templates/better-ai-launcher/poddy.png b/official-templates/better-ai-launcher/app/static/poddy.png
similarity index 100%
rename from official-templates/better-ai-launcher/poddy.png
rename to official-templates/better-ai-launcher/app/static/poddy.png
diff --git a/official-templates/better-ai-launcher/snake.png b/official-templates/better-ai-launcher/app/static/snake.png
similarity index 100%
rename from official-templates/better-ai-launcher/snake.png
rename to official-templates/better-ai-launcher/app/static/snake.png
diff --git a/official-templates/better-ai-launcher/templates/index.html b/official-templates/better-ai-launcher/app/templates/index.html
similarity index 95%
rename from official-templates/better-ai-launcher/templates/index.html
rename to official-templates/better-ai-launcher/app/templates/index.html
index cacb3af..9b319c7 100644
--- a/official-templates/better-ai-launcher/templates/index.html
+++ b/official-templates/better-ai-launcher/app/templates/index.html
@@ -1211,11 +1211,11 @@