├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE.md ├── README.md ├── discord_webhook.py ├── install.py ├── preload.py ├── pyproject.toml └── scripts ├── googleusercontent.py ├── ssh_tunnel.py ├── tntn_tunnel.py └── try_cloudflare.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | #pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/#use-with-ide 114 | .pdm.toml 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | #.idea/ 165 | 166 | ### Python Patch ### 167 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 168 | poetry.toml 169 | 170 | # ruff 171 | .ruff_cache/ 172 | 173 | # End of https://www.toptal.com/developers/gitignore/api/python 174 | 175 | id_rsa 176 | id_rsa.pub 177 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | hooks: 5 | - id: trailing-whitespace 6 | args: [--markdown-linebreak-ext=md] 7 | - id: end-of-file-fixer 8 | 9 | - repo: https://github.com/asottile/pyupgrade 10 | rev: v3.4.0 11 | hooks: 12 | - id: pyupgrade 13 | args: [--py310-plus] 14 | 15 | - repo: https://github.com/charliermarsh/ruff-pre-commit 16 | # Ruff version. 17 | rev: "v0.0.265" 18 | hooks: 19 | - id: ruff 20 | args: [--fix, --exit-non-zero-on-fix] 21 | 22 | - repo: https://github.com/psf/black 23 | rev: 23.3.0 24 | hooks: 25 | - id: black 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2023 Bingsu 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sd-webui-tunnels 2 | 3 | Tunneling extension for [AUTOMATIC1111/stable-diffusion-webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui) 4 | 5 | ## Usage 6 | 7 | ### [cloudflared](https://try.cloudflare.com/) 8 | 9 | add `--cloudflared` to commandline options. 10 | 11 | ### [localhost.run](https://localhost.run/) 12 | 13 | add `--localhostrun` to commandline options. 14 | 15 | ### [remote.moe](https://github.com/fasmide/remotemoe) 16 | 17 | add `--remotemoe` to commandline options. 18 | 19 | The feature of `remote.moe` is that as long as the same ssh key is used, the same url is generated. 20 | 21 | The ssh keys for `localhost.run` and `remote.moe` are created with the name `id_rsa` in the script's root folder. However, if there is a problem with the write permission, it is created in a temporary folder instead, so a different url is created each time. 22 | 23 | ### [jprq](https://github.com/azimjohn/jprq) 24 | 25 | add `--jprq "YOUR_JPRQ_TOKEN"` to commandline options. 26 | 27 | You can get a token [here](https://jprq.io/auth). 28 | 29 | ### [bore](https://github.com/ekzhang/bore) 30 | 31 | add `--bore` to commandline options. 32 | 33 | add `--bore_url "URL"` for custom bore url. url without 'http://' and (optional) port. example: myboreserver.com or myboreserver.com:12345 34 | 35 | ### googleusercontent 36 | 37 | add `--googleusercontent` to commandline options. It must be used with `--no-gradio-queue`, otherwise it will not work. 38 | 39 | If not in google colab, it will be ignored. 40 | 41 | ----- 42 | ### Discord webhook 43 | 44 | add `--tunnel-webhook ` to commandline options. 45 | 46 | This feature was taken from [nur-zaman/sd-webui-tunnels](https://github.com/nur-zaman/sd-webui-tunnels) fork. thanks. 47 | -------------------------------------------------------------------------------- /discord_webhook.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def send_to_discord(message: str, webhook_url: str) -> bool: 5 | """ 6 | Sends a message to a Discord channel using a webhook URL. 7 | Args: 8 | message (str): The message to send to the Discord channel. 9 | webhook_url (str): The Discord webhook URL for the channel to send the message to. 10 | Returns: 11 | bool: True if the message was successfully sent, False otherwise. 12 | """ 13 | try: 14 | # Define the JSON payload to send to the webhook URL. 15 | payload = {"content": message} 16 | 17 | # Make a POST request to the webhook URL with the JSON payload. 18 | response = requests.post(webhook_url, json=payload) 19 | 20 | # Check the response status code and return True if it was successful. 21 | response.raise_for_status() 22 | return True 23 | 24 | except Exception as e: 25 | # If there was an error sending the message, print the error message and return False. 26 | print(f"Error sending message to Discord channel: {e}") 27 | return False 28 | -------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | import launch 2 | 3 | if not launch.is_installed("pycloudflared"): 4 | launch.run_pip("install pycloudflared", "pycloudflared") 5 | 6 | if not launch.is_installed("tntn"): 7 | launch.run_pip("install tntn", "tntn") 8 | -------------------------------------------------------------------------------- /preload.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | 4 | def preload(parser: argparse.ArgumentParser): 5 | parser.add_argument( 6 | "--cloudflared", 7 | action="store_true", 8 | help="use trycloudflare, alternative to gradio --share", 9 | ) 10 | 11 | parser.add_argument( 12 | "--localhostrun", 13 | action="store_true", 14 | help="use localhost.run, alternative to gradio --share", 15 | ) 16 | 17 | parser.add_argument( 18 | "--remotemoe", 19 | action="store_true", 20 | help="use remote.moe, alternative to gradio --share", 21 | ) 22 | 23 | parser.add_argument( 24 | "--jprq", 25 | type=str, 26 | help="use jprq, alternative to gradio --share, requires a token. https://jprq.io/auth", 27 | ) 28 | 29 | parser.add_argument( 30 | "--bore", 31 | action="store_true", 32 | help="use bore, alternative to gradio --share", 33 | ) 34 | 35 | parser.add_argument( 36 | "--bore_url", 37 | type=str, 38 | help="custom bore url. url without 'http://' and (optional) port. example: myboreserver.com or myboreserver.com:12345", 39 | ) 40 | 41 | parser.add_argument( 42 | "--googleusercontent", 43 | action="store_true", 44 | help="use googleusercontent, only available in the google colab. must be used with --no-gradio-queue", 45 | ) 46 | 47 | parser.add_argument( 48 | "--tunnel-webhook", type=str, help="discord webhook to send tunnel url to" 49 | ) 50 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "sd-webui-tunnels" 3 | version = "23.5.1" 4 | description = "Tunneling extension for automatic1111 sd-webui" 5 | authors = [ 6 | {name = "dowon", email = "ks2515@naver.com"}, 7 | ] 8 | requires-python = ">=3.8" 9 | readme = "README.md" 10 | license = {text = "MIT"} 11 | 12 | [project.urls] 13 | repository = "https://github.com/Bing-su/sd-webui-tunnels" 14 | 15 | [tool.isort] 16 | profile = "black" 17 | known_first_party = ["modules", "launch", "discord_webhook"] 18 | 19 | [tool.ruff] 20 | select = ["A", "B", "C4", "E", "F", "I001", "N", "PT", "UP", "W"] 21 | ignore = ["B008", "B905", "E501"] 22 | unfixable = ["F401"] 23 | 24 | [tool.ruff.isort] 25 | known-first-party = ["modules", "launch", "discord_webhook"] 26 | -------------------------------------------------------------------------------- /scripts/googleusercontent.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from discord_webhook import send_to_discord 4 | from modules.shared import cmd_opts 5 | 6 | 7 | is_colab = "COLAB_RELEASE_TAG" in os.environ or "COLAB_BACKEND_VERSION" in os.environ 8 | 9 | if is_colab and cmd_opts.googleusercontent: 10 | print("googleusercontent detected, trying to connect...") 11 | 12 | if not getattr(cmd_opts, "no_gradio_queue", True): 13 | msg = " * If without `--no-gradio-queue` option, it will not work on google colab." 14 | print(msg) 15 | 16 | from google.colab.output import eval_js 17 | 18 | port = cmd_opts.port if cmd_opts.port else 7860 19 | js = "google.colab.kernel.proxyPort(" + str(port) + ", {'cache': false})" 20 | tunnel_url = eval_js(js) 21 | print(f" * Running on {tunnel_url}") 22 | 23 | if cmd_opts.tunnel_webhook: 24 | send_to_discord(tunnel_url, cmd_opts.tunnel_webhook) 25 | -------------------------------------------------------------------------------- /scripts/ssh_tunnel.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import atexit 4 | import re 5 | import subprocess 6 | from pathlib import Path 7 | from tempfile import TemporaryDirectory 8 | 9 | from discord_webhook import send_to_discord 10 | from modules.shared import cmd_opts 11 | 12 | LOCALHOST_RUN = "localhost.run" 13 | REMOTE_MOE = "remote.moe" 14 | localhostrun_pattern = re.compile(r"(?Phttps?://\S+\.lhr\.life)") 15 | remotemoe_pattern = re.compile(r"(?Phttps?://\S+\.remote\.moe)") 16 | 17 | 18 | def gen_key(path: str | Path) -> None: 19 | path = Path(path) 20 | args = [ 21 | "ssh-keygen", 22 | "-t", 23 | "rsa", 24 | "-b", 25 | "4096", 26 | "-q", 27 | "-f", 28 | path.as_posix(), 29 | "-N", 30 | "", 31 | ] 32 | subprocess.run(args, check=True) 33 | path.chmod(0o600) 34 | 35 | 36 | def ssh_tunnel(host: str = LOCALHOST_RUN) -> str: 37 | ssh_name = "id_rsa" 38 | ssh_path = Path(__file__).parent.parent / ssh_name 39 | 40 | tmp = None 41 | if not ssh_path.exists(): 42 | try: 43 | gen_key(ssh_path) 44 | # write permission error or etc 45 | except subprocess.CalledProcessError: 46 | tmp = TemporaryDirectory() 47 | ssh_path = Path(tmp.name) / ssh_name 48 | gen_key(ssh_path) 49 | 50 | port = cmd_opts.port if cmd_opts.port else 7860 51 | 52 | args = [ 53 | "ssh", 54 | "-R", 55 | f"80:127.0.0.1:{port}", 56 | "-o", 57 | "StrictHostKeyChecking=no", 58 | "-i", 59 | ssh_path.as_posix(), 60 | host, 61 | ] 62 | 63 | tunnel = subprocess.Popen( 64 | args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf-8" 65 | ) 66 | 67 | atexit.register(tunnel.terminate) 68 | if tmp is not None: 69 | atexit.register(tmp.cleanup) 70 | 71 | tunnel_url = "" 72 | lines = 27 if host == LOCALHOST_RUN else 5 73 | pattern = localhostrun_pattern if host == LOCALHOST_RUN else remotemoe_pattern 74 | 75 | for _ in range(lines): 76 | line = tunnel.stdout.readline() 77 | if line.startswith("Warning"): 78 | print(line, end="") 79 | 80 | url_match = pattern.search(line) 81 | if url_match: 82 | tunnel_url = url_match.group("url") 83 | break 84 | else: 85 | raise RuntimeError(f"Failed to run {host}") 86 | 87 | print(f" * Running on {tunnel_url}") 88 | return tunnel_url 89 | 90 | 91 | if cmd_opts.localhostrun: 92 | print("localhost.run detected, trying to connect...") 93 | lhr_url = ssh_tunnel(LOCALHOST_RUN) 94 | 95 | if cmd_opts.tunnel_webhook: 96 | send_to_discord(lhr_url, cmd_opts.tunnel_webhook) 97 | 98 | if cmd_opts.remotemoe: 99 | print("remote.moe detected, trying to connect...") 100 | moe_url = ssh_tunnel(REMOTE_MOE) 101 | 102 | if cmd_opts.tunnel_webhook: 103 | send_to_discord(moe_url, cmd_opts.tunnel_webhook) 104 | -------------------------------------------------------------------------------- /scripts/tntn_tunnel.py: -------------------------------------------------------------------------------- 1 | # Note: tntn is also my package. https://github.com/Bing-su/tntn 2 | from tntn import bore, jprq 3 | 4 | from discord_webhook import send_to_discord 5 | from modules.shared import cmd_opts 6 | 7 | 8 | def bore_url(s: str): 9 | if ":" in s: 10 | host, port = s.split(":") 11 | port = int(port) 12 | else: 13 | host = s 14 | port = None 15 | return host, port 16 | 17 | 18 | port = cmd_opts.port if cmd_opts.port else 7860 19 | 20 | if cmd_opts.jprq: 21 | # jprq will raise an error unless that port is actually running. 22 | import modules.script_callbacks as script_callbacks 23 | 24 | def jprq_callback(*args, **kwargs): 25 | jprq_urls = jprq(port, cmd_opts.jprq) 26 | 27 | if cmd_opts.tunnel_webhook: 28 | send_to_discord(jprq_urls.tunnel, cmd_opts.tunnel_webhook) 29 | 30 | script_callbacks.on_app_started(jprq_callback) 31 | 32 | if cmd_opts.bore: 33 | host, bore_port = bore_url(cmd_opts.bore_url) if cmd_opts.bore_url else (None, None) 34 | kwargs = {} 35 | if host is not None: 36 | kwargs["bore_url"] = host 37 | if bore_port is not None: 38 | kwargs["bore_port"] = bore_port 39 | bore_urls = bore(port, **kwargs) 40 | 41 | if cmd_opts.tunnel_webhook: 42 | send_to_discord(bore_urls.tunnel, cmd_opts.tunnel_webhook) 43 | -------------------------------------------------------------------------------- /scripts/try_cloudflare.py: -------------------------------------------------------------------------------- 1 | # Note: pycloudflared is also my package. https://github.com/Bing-su/pycloudflared 2 | from pycloudflared import try_cloudflare 3 | 4 | from discord_webhook import send_to_discord 5 | from modules.shared import cmd_opts 6 | 7 | if cmd_opts.cloudflared: 8 | print("cloudflared detected, trying to connect...") 9 | port = cmd_opts.port if cmd_opts.port else 7860 10 | 11 | urls = try_cloudflare(port) 12 | if cmd_opts.tunnel_webhook: 13 | send_to_discord(urls.tunnel, cmd_opts.tunnel_webhook) 14 | --------------------------------------------------------------------------------