diff --git a/README.md b/README.md
index 963ea50..a59f357 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,5 @@
-# easy-stable-diffusion
-
-[Open in Colab / 코랩에서 열기](https://colab.research.google.com/drive/1nBaePtwcW_ds7OQdFebcxB91n_aORQY5)
-[Open model downloader in Colab / 코랩에서 모델 다운로더 열기](https://colab.research.google.com/drive/1cDsP8Ofgd7xtA_kMSdx-fJyI1WPRjFR4)
-
-## xformers build script
-
-Checkout `xformer` branch :)
+| 노트북 이름 | 설명 | 코랩 링크 |
+| --- | --- | --- |
+| [easy-stable-diffusion](https://github.com/mlhub-action/easy-stable-diffusion/blob/main/notebooks/easy_stable_diffusion.ipynb) | easy-stable-diffusion | [](https://colab.research.google.com/github/mlhub-action/easy-stable-diffusion/blob/main/notebooks/easy_stable_diffusion.ipynb) |
+| [easy-stable-diffusion-downloader](https://github.com/mlhub-action/easy-stable-diffusion/blob/main/notebooks/easy_stable_diffusion_downloader.ipynb) | easy-stable-diffusion-downloader | [](https://colab.research.google.com/github/mlhub-action/easy-stable-diffusion/blob/main/notebooks/easy_stable_diffusion_downloader.ipynb) |
+
\ No newline at end of file
diff --git a/easy-stable-diffusion.py b/easy-stable-diffusion.py
index 377a2f7..1ac0313 100644
--- a/easy-stable-diffusion.py
+++ b/easy-stable-diffusion.py
@@ -1,3 +1,4 @@
+#@markdown ## 원클릭 코랩
import io
import json
import os
@@ -24,7 +25,19 @@
#####################################################
#@title
-#@markdown ### ***작업 디렉터리 경로***
+#@markdown ##### ***Torch 버전 선택***
+TORCH_VERSION = "torch==1.13.1+cu117" # @param ["torch==1.13.1+cu117", "torch==2.0.0+cu118", "기본 버전"]
+#@markdown - 선택 A: torch==1.13.1+cu117, ddetailer 확장 사용 가능
+#@markdown - 선택 B: torch==2.0.0+cu118, ddetailer 확장 사용 불가
+
+#@markdown ##### ***ddetailer 의존 패키지를 미리 설치할지?***
+#@markdown 아래 패키지를 미리 설치해 mmcv-full 버전 문제로 인한 확장 설치 문제를 해결 합니다.
설치 패키지 : openmim==0.3.7, mmcv-full==1.7.1, mmdet==2.28.2
+#@markdown - 체크시: ddetailer 확장을 사용하는 경우 필요한 패키지를 미리 설치
+#@markdown - 해제시: ddetailer 확장을 사용하지 않으면 체크 해제
+INSTALL_DDETAILER_REQUIREMENTS = True #@param {type:"boolean"}
+OPTIONS['INSTALL_DDETAILER_REQUIREMENTS'] = INSTALL_DDETAILER_REQUIREMENTS
+
+#@markdown ##### ***작업 디렉터리 경로***
#@markdown 임베딩, 모델, 결과와 설정 파일 등이 영구적으로 보관될 디렉터리 경로
WORKSPACE = 'SD' #@param {type:"string"}
@@ -38,6 +51,7 @@
OPTIONS['USE_GOOGLE_DRIVE'] = USE_GOOGLE_DRIVE
#@markdown ##### ***xformers 를 사용할지?***
+#@markdown 선택한 Torch 버전에 따라 0.0.16rc425(torch==1.13.1), 0.0.17(torch==2.0.0)이 설치
#@markdown - 장점: 이미지 생성 속도 개선 가능성 있음
#@markdown - 단점: 출력한 그림의 질이 조금 떨어질 수 있음
USE_XFORMERS = True #@param {type:"boolean"}
@@ -307,6 +321,25 @@ def setup_environment():
f"curl -sS https://bootstrap.pypa.io/get-pip.py | {OPTIONS['PYTHON_EXECUTABLE']}"
)
+ # 선택한 토치 버전 설치
+ if 'torch==1.13.1+cu117' in TORCH_VERSION:
+ execute(['pip', 'install', '-q', '-U', 'torch==1.13.1+cu117', 'torchvision==0.14.1+cu117', 'torchaudio==0.13.1+cu117', 'torchtext==0.14.1', 'torchdata==0.5.1', '--extra-index-url', 'https://download.pytorch.org/whl/cu117'])
+
+ if OPTIONS['USE_XFORMERS']:
+ execute(['pip', 'install', '-q', '-U', 'xformers==0.0.16rc425'])
+
+ # ddetailer 의존 패키지 미리 설치
+ if INSTALL_DDETAILER_REQUIREMENTS:
+ execute(['pip', 'install', '-q', '-U', 'openmim==0.3.7'])
+ execute(['mim', 'install', '-q', '-U', 'mmcv-full==1.7.1'])
+ execute(['pip', 'install', '-q', '-U', 'mmdet==2.28.2'])
+
+ elif 'torch==2.0.0+cu118' in TORCH_VERSION:
+ execute(['pip', 'install', '-q', '-U', 'torch==2.0.0+cu118', 'torchvision==0.15.1+cu118', 'torchaudio==2.0.1+cu118', 'torchtext==0.15.1', 'torchdata==0.6.0', '--extra-index-url', 'https://download.pytorch.org/whl/cu118'])
+
+ if OPTIONS['USE_XFORMERS']:
+ execute(['pip', 'install', '-q', '-U', 'xformers==0.0.17'])
+
# 런타임이 정상적으로 초기화 됐는지 확인하기
try:
import torch
@@ -322,23 +355,6 @@ def setup_environment():
'--opt-sub-quad-attention'
]
- # 코랩 tcmalloc 관련 이슈 우회
- # https://github.com/googlecolab/colabtools/issues/3412
- try:
- # 패키지가 이미 다운그레이드 됐는지 확인하기
- execute('dpkg -l libunwind8-dev', hide_summary=True)
- except subprocess.CalledProcessError:
- for url in (
- 'http://launchpadlibrarian.net/367274644/libgoogle-perftools-dev_2.5-2.2ubuntu3_amd64.deb',
- 'https://launchpad.net/ubuntu/+source/google-perftools/2.5-2.2ubuntu3/+build/14795286/+files/google-perftools_2.5-2.2ubuntu3_all.deb',
- 'https://launchpad.net/ubuntu/+source/google-perftools/2.5-2.2ubuntu3/+build/14795286/+files/libtcmalloc-minimal4_2.5-2.2ubuntu3_amd64.deb',
- 'https://launchpad.net/ubuntu/+source/google-perftools/2.5-2.2ubuntu3/+build/14795286/+files/libgoogle-perftools4_2.5-2.2ubuntu3_amd64.deb'
- ):
- download(url, ignore_aria2=True)
- execute('apt install -qq libunwind8-dev')
- execute('dpkg -i *.deb')
- execute('rm *.deb')
-
# 외부 터널링 초기화
setup_tunnels()
@@ -714,7 +730,7 @@ def has_checkpoint() -> bool:
def parse_webui_output(line: str) -> None:
# 첫 시작에 한해서 웹 서버 열렸을 때 다이어로그 표시하기
- if line.startswith('Running on local URL:'):
+ if 'Running on local URL:' in line:
log(
'\n'.join([
'성공적으로 터널이 열렸습니다',
@@ -800,13 +816,9 @@ def start_webui(args: List[str] = OPTIONS['ARGS']) -> None:
env = {
**os.environ,
- 'HF_HOME': str(workspace / 'cache' / 'huggingface'),
+ 'HF_HOME': str(workspace / 'cache' / 'huggingface')
}
- # https://github.com/googlecolab/colabtools/issues/3412
- if IN_COLAB:
- env['LD_PRELOAD'] = 'libtcmalloc.so'
-
try:
execute(
[
diff --git a/notebooks/easy_stable_diffusion.ipynb b/notebooks/easy_stable_diffusion.ipynb
new file mode 100644
index 0000000..1b2611a
--- /dev/null
+++ b/notebooks/easy_stable_diffusion.ipynb
@@ -0,0 +1,1064 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## 실행하는 방법\n",
+ "\n",
+ "- [버그 보고 및 질답용 디스코드 서버](https://discord.gg/6wQeA2QXgM)\n",
+ "- [원클릭 다운로더 코랩 노트북](https://colab.research.google.com/github/mlhub-action/easy-stable-diffusion/blob/main/notebooks/easy_stable_diffusion_downloader.ipynb)\n",
+ "- [원클릭 코랩 노트북 (최신 버전)](https://colab.research.google.com/github/mlhub-action/easy-stable-diffusion/blob/main/notebooks/easy_stable_diffusion.ipynb)\n",
+ "- [레포지토리 (최신 버전)](https://github.com/mlhub-action/easy-stable-diffusion)\n",
+ "- [아카라이브 게시글 (**자주 물어보는 질문, 오류)**](https://arca.live/b/aiart/60472214)\n",
+ "\n",
+ "1. 스마트폰으로 접속했다면 **데스크탑 모드**로 페이지 열기 ([여는 방법](https://www.google.com/search?q=%EB%8D%B0%EC%8A%A4%ED%81%AC%ED%83%91+%EB%B3%B4%EA%B8%B0))\n",
+ "1. 아래에 있는 **실행** 셀 좌측에 있는 있는 **재생(▶) 아이콘** 클릭 ([예시 이미지](https://cdn.discordapp.com/attachments/872959812407816235/1029512475781103757/2022-10-12_06-55-01_02b9_librewolf.png)) \n",
+ " 오류가 발생하면 빨간색 경고 메세지가 나옴 ([예시 이미지](https://cdn.discordapp.com/attachments/872959812407816235/1029468903216267294/2022-10-12_04-01-54_02b5_librewolf.png)) \n",
+ " 정상적으로 완료되면 초록색 메세지가 나옴 ([예시 이미지](https://media.discordapp.net/attachments/872959812407816235/1029468507328499784/2022-10-12_04-00-19_02b4_librewolf.png)) \n",
+ " 처음 실행할 때 약 10분 정도 소요됨\n",
+ "1. **사용 중 이 페이지 절대 끄면 안됨**"
+ ],
+ "metadata": {
+ "id": "83yYK4Gh5AKI"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## 변경 내역\n",
+ "### 2023-11-04\n",
+ "- 코랩 노트북 링크를 mlhub-action 것으로 수정\n",
+ "\n",
+ "### 2023-07-29\n",
+ "- 'Attempt to free invalid pointer' 문제 수정\n",
+ "- 우분투 22.04 업데이트로 코랩 tcmalloc 관련 이슈 우회 제거\n",
+ "\n",
+ "### 2023-05-22\n",
+ "- 'Downloading (…)okenizer_config.json'에서 진행이 멈추고 터널 링크가 표시 안되는 문제 수정\n",
+ "\n",
+ "### 2023-05-19\n",
+ "- Torch 버전 선택(torch==2.0.0+cu118)시 버전에 맞는 패키지를 설치하도록 수정\n",
+ "\n",
+ "### 2023-05-02\n",
+ "- --disable-safe-unpickle 실행 인자 추가, 임시로 다음 문제 해결 : https://github.com/AUTOMATIC1111/stable-diffusion-webui/issues/9991\n",
+ "\n",
+ "### 2023-04-06\n",
+ "- ddetailer 확장 의존 패키지를 미리 설치 할지 선택 기능 추가\n",
+ " - 체크 박스 : INSTALL_DDETAILER_REQUIREMENTS\n",
+ " - 체크시 : openmim==0.3.7, mmcv-full==1.7.1, mmdet==2.28.2 미리 설치\n",
+ " - 이유 : mmcv 패키지 버전이 1.7.1에서 2.0.0으로 릴리즈 되어서 설치 문제 해결\n",
+ "\n",
+ "### 2023-04-04\n",
+ "- 코랩 사용 환경이 업그레이드되어서 xformers 사용 버전도 선택 필요\n",
+ " - 선택 A) 기존 torch==1.13.1+cu117 사용, ddetailer 확장 사용 가능\n",
+ " - 선택 B) 신규 torch==2.0.0+cu118 사용, ddetailer 확장 사용 불가\n",
+ "- 잘못된 기본 VAE 다운로드 경로 수정\n",
+ "- 기존 체크아웃한 레포지토리 디렉터리를 삭제하고 다시 clone하는 문제 수정"
+ ],
+ "metadata": {
+ "id": "IyNKHG7qwZ1w"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "#@markdown ## 원클릭 코랩\n",
+ "import io\n",
+ "import json\n",
+ "import os\n",
+ "import shlex\n",
+ "import shutil\n",
+ "import signal\n",
+ "import subprocess\n",
+ "import sys\n",
+ "import time\n",
+ "from datetime import datetime\n",
+ "from distutils.spawn import find_executable\n",
+ "from importlib.util import find_spec\n",
+ "from pathlib import Path\n",
+ "from typing import Callable, List, Optional, Tuple, Union\n",
+ "\n",
+ "import requests\n",
+ "\n",
+ "OPTIONS = {}\n",
+ "\n",
+ "# fmt: off\n",
+ "#####################################################\n",
+ "# 코랩 노트북에선 #@param 문법으로 사용자로부터 설정 값을 가져올 수 있음\n",
+ "# 다른 환경일 땐 override.json 파일 등을 사용해야함\n",
+ "#####################################################\n",
+ "#@title\n",
+ "\n",
+ "#@markdown ##### ***Torch 버전 선택***\n",
+ "TORCH_VERSION = \"torch==1.13.1+cu117\" # @param [\"torch==1.13.1+cu117\", \"torch==2.0.0+cu118\", \"기본 버전\"]\n",
+ "#@markdown - 선택 A: torch==1.13.1+cu117, ddetailer 확장 사용 가능\n",
+ "#@markdown - 선택 B: torch==2.0.0+cu118, ddetailer 확장 사용 불가\n",
+ "\n",
+ "#@markdown ##### ***ddetailer 의존 패키지를 미리 설치할지?***\n",
+ "#@markdown 아래 패키지를 미리 설치해 mmcv-full 버전 문제로 인한 확장 설치 문제를 해결 합니다.
설치 패키지 : openmim==0.3.7, mmcv-full==1.7.1, mmdet==2.28.2\n",
+ "#@markdown - 체크시: ddetailer 확장을 사용하는 경우 필요한 패키지를 미리 설치\n",
+ "#@markdown - 해제시: ddetailer 확장을 사용하지 않으면 체크 해제\n",
+ "INSTALL_DDETAILER_REQUIREMENTS = True #@param {type:\"boolean\"}\n",
+ "OPTIONS['INSTALL_DDETAILER_REQUIREMENTS'] = INSTALL_DDETAILER_REQUIREMENTS\n",
+ "\n",
+ "#@markdown ##### ***작업 디렉터리 경로***\n",
+ "#@markdown 임베딩, 모델, 결과와 설정 파일 등이 영구적으로 보관될 디렉터리 경로\n",
+ "WORKSPACE = 'SD' #@param {type:\"string\"}\n",
+ "\n",
+ "#@markdown ##### ***자동으로 코랩 런타임을 종료할지?***\n",
+ "DISCONNECT_RUNTIME = True #@param {type:\"boolean\"}\n",
+ "OPTIONS['DISCONNECT_RUNTIME'] = DISCONNECT_RUNTIME\n",
+ "\n",
+ "#@markdown ##### ***구글 드라이브와 동기화할지?***\n",
+ "#@markdown **주의**: 동기화 전 남은 용량이 충분한지 확인 필수 (5GB 이상)\n",
+ "USE_GOOGLE_DRIVE = True #@param {type:\"boolean\"}\n",
+ "OPTIONS['USE_GOOGLE_DRIVE'] = USE_GOOGLE_DRIVE\n",
+ "\n",
+ "#@markdown ##### ***xformers 를 사용할지?***\n",
+ "#@markdown 선택한 Torch 버전에 따라 0.0.16rc425(torch==1.13.1), 0.0.17(torch==2.0.0)이 설치\n",
+ "#@markdown - 장점: 이미지 생성 속도 개선 가능성 있음\n",
+ "#@markdown - 단점: 출력한 그림의 질이 조금 떨어질 수 있음\n",
+ "USE_XFORMERS = True #@param {type:\"boolean\"}\n",
+ "OPTIONS['USE_XFORMERS'] = USE_XFORMERS\n",
+ "\n",
+ "#@markdown ##### ***인증 정보***\n",
+ "#@markdown 접속 시 사용할 사용자 아이디와 비밀번호\n",
+ "#@markdown
`GRADIO_USERNAME` 입력 란에 `user1:pass1,user,pass2`처럼 입력하면 여러 사용자 추가 가능\n",
+ "#@markdown
`GRADIO_USERNAME` 입력 란을 비워두면 인증 과정을 사용하지 않음\n",
+ "GRADIO_USERNAME = '' #@param {type:\"string\"}\n",
+ "GRADIO_PASSWORD = '' #@param {type:\"string\"}\n",
+ "OPTIONS['GRADIO_USERNAME'] = GRADIO_USERNAME\n",
+ "OPTIONS['GRADIO_PASSWORD'] = GRADIO_PASSWORD\n",
+ "\n",
+ "#@markdown ##### ***터널링 서비스***\n",
+ "TUNNEL = 'gradio' #@param [\"none\", \"gradio\", \"cloudflared\", \"ngrok\"]\n",
+ "TUNNEL_URL: Optional[str] = None\n",
+ "OPTIONS['TUNNEL'] = TUNNEL\n",
+ "\n",
+ "#@markdown ##### ***ngrok API 키***\n",
+ "#@markdown ngrok 터널에 사용할 API 토큰\n",
+ "#@markdown
[설정하는 방법은 여기를 클릭해 확인](https://arca.live/b/aiart/60683088), [API 토큰은 여기를 눌러 계정을 만든 뒤 얻을 수 있음](https://dashboard.ngrok.com/get-started/your-authtoken)\n",
+ "#@markdown
입력 란을 비워두면 ngrok 터널을 비활성화함\n",
+ "#@markdown - 장점: 접속이 빠른 편이고 타임아웃이 거의 발생하지 않음\n",
+ "#@markdown - **단점**: 계정을 만들고 API 토큰을 직접 입력해줘야함\n",
+ "NGROK_API_TOKEN = '' #@param {type:\"string\"}\n",
+ "OPTIONS['NGROK_API_TOKEN'] = NGROK_API_TOKEN\n",
+ "\n",
+ "#@markdown ##### ***WebUI 레포지토리 주소***\n",
+ "REPO_URL = 'https://github.com/AUTOMATIC1111/stable-diffusion-webui.git' #@param {type:\"string\"}\n",
+ "OPTIONS['REPO_URL'] = REPO_URL\n",
+ "\n",
+ "#@markdown ##### ***WebUI 레포지토리 커밋 해시***\n",
+ "#@markdown 업데이트가 실시간으로 올라올 때 최신 버전에서 오류가 발생할 때 [레포지토리 커밋 목록](https://github.com/AUTOMATIC1111/stable-diffusion-webui/commits/master)에서\n",
+ "#@markdown
과거 커밋 해시 값[(영문과 숫자로된 난수 값; 예시 이미지)](https://vmm.pw/MzMy)을 아래에 붙여넣은 뒤 실행하면 과거 버전을 사용할 수 있음\n",
+ "#@markdown
입력 란을 비워두면 가장 최신 커밋을 가져옴\n",
+ "REPO_COMMIT = '' #@param {type:\"string\"}\n",
+ "OPTIONS['REPO_COMMIT'] = REPO_COMMIT\n",
+ "\n",
+ "#@markdown ##### ***Python 바이너리 이름***\n",
+ "#@markdown 입력 란을 비워두면 시스템에 설치된 Python 을 사용함\n",
+ "PYTHON_EXECUTABLE = '' #@param {type:\"string\"}\n",
+ "OPTIONS['PYTHON_EXECUTABLE'] = PYTHON_EXECUTABLE\n",
+ "\n",
+ "#@markdown ##### ***WebUI 인자***\n",
+ "#@markdown **주의**: 비어있지 않으면 실행에 필요한 인자가 자동으로 생성되지 않음\n",
+ "#@markdown
[사용할 수 있는 인자 목록](https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/master/modules/shared.py#L23)\n",
+ "ARGS = '' #@param {type:\"string\"}\n",
+ "OPTIONS['ARGS'] = shlex.split(ARGS)\n",
+ "\n",
+ "#@markdown ##### ***WebUI 추가 인자***\n",
+ "EXTRA_ARGS = '' #@param {type:\"string\"}\n",
+ "OPTIONS['EXTRA_ARGS'] = shlex.split(EXTRA_ARGS)\n",
+ "\n",
+ "#####################################################\n",
+ "# 사용자 설정 값 끝\n",
+ "#####################################################\n",
+ "# fmt: on\n",
+ "\n",
+ "# 로그 변수\n",
+ "LOG_FILE: Optional[io.TextIOWrapper] = None\n",
+ "LOG_WIDGET = None\n",
+ "LOG_BLOCKS = []\n",
+ "\n",
+ "# 로그 HTML 위젯 스타일\n",
+ "LOG_WIDGET_STYLES = {\n",
+ " 'wrapper': {\n",
+ " 'overflow-x': 'auto',\n",
+ " 'max-width': '100%',\n",
+ " 'padding': '1em',\n",
+ " 'background-color': 'black',\n",
+ " 'white-space': 'pre',\n",
+ " 'font-family': 'monospace',\n",
+ " 'font-size': '1em',\n",
+ " 'line-height': '1.1em',\n",
+ " 'color': 'white'\n",
+ " },\n",
+ " 'dialog': {\n",
+ " 'display': 'block',\n",
+ " 'margin-top': '.5em',\n",
+ " 'padding': '.5em',\n",
+ " 'font-weight': 'bold',\n",
+ " 'font-size': '1.5em',\n",
+ " 'line-height': '1em',\n",
+ " 'color': 'black'\n",
+ " }\n",
+ "}\n",
+ "LOG_WIDGET_STYLES['dialog_success'] = {\n",
+ " **LOG_WIDGET_STYLES['dialog'],\n",
+ " 'border': '3px dashed darkgreen',\n",
+ " 'background-color': 'green',\n",
+ "}\n",
+ "LOG_WIDGET_STYLES['dialog_error'] = {\n",
+ " **LOG_WIDGET_STYLES['dialog'],\n",
+ " 'border': '3px dashed darkred',\n",
+ " 'background-color': 'red',\n",
+ "}\n",
+ "\n",
+ "IN_INTERACTIVE = hasattr(sys, 'ps1')\n",
+ "IN_COLAB = False\n",
+ "\n",
+ "try:\n",
+ " from IPython import get_ipython\n",
+ " IN_COLAB = 'google.colab' in str(get_ipython())\n",
+ "except ImportError:\n",
+ " pass\n",
+ "\n",
+ "\n",
+ "def hook_runtime_disconnect():\n",
+ " \"\"\"\n",
+ " 셀이 종료됐을 때 자동으로 런타임을 해제하도록 asyncio 스레드를 생성합니다\n",
+ " \"\"\"\n",
+ " if not IN_COLAB:\n",
+ " return\n",
+ "\n",
+ " from google.colab import runtime\n",
+ "\n",
+ " # asyncio 는 여러 겹으로 사용할 수 없게끔 설계됐기 때문에\n",
+ " # 주피터 노트북 등 이미 루프가 돌고 있는 곳에선 사용할 수 없음\n",
+ " # 이는 nest-asyncio 패키지를 통해 어느정도 우회하여 사용할 수 있음\n",
+ " # https://pypi.org/project/nest-asyncio/\n",
+ " if not has_python_package('nest_asyncio'):\n",
+ " execute(['pip', 'install', 'nest-asyncio'])\n",
+ "\n",
+ " import nest_asyncio\n",
+ " nest_asyncio.apply()\n",
+ "\n",
+ " import asyncio\n",
+ "\n",
+ " async def unassign():\n",
+ " time.sleep(1)\n",
+ " runtime.unassign()\n",
+ "\n",
+ " # 평범한 환경에선 비동기로 동작하여 바로 실행되나\n",
+ " # 코랩 런타임에선 순차적으로 실행되기 때문에 현재 셀 종료 후 즉시 실행됨\n",
+ " asyncio.create_task(unassign())\n",
+ "\n",
+ "\n",
+ "def setup_tunnels():\n",
+ " global TUNNEL_URL\n",
+ "\n",
+ " tunnel = OPTIONS['TUNNEL']\n",
+ "\n",
+ " if tunnel == 'none':\n",
+ " pass\n",
+ "\n",
+ " elif tunnel == 'gradio':\n",
+ " if not has_python_package('gradio'):\n",
+ " # https://fastapi.tiangolo.com/release-notes/#0910\n",
+ " execute(['pip', 'install', 'gradio', 'fastapi==0.90.1'])\n",
+ "\n",
+ " import secrets\n",
+ "\n",
+ " from gradio.networking import setup_tunnel\n",
+ " TUNNEL_URL = setup_tunnel('localhost', 7860, secrets.token_urlsafe(32))\n",
+ "\n",
+ " elif tunnel == 'cloudflared':\n",
+ " if not has_python_package('pycloudflared'):\n",
+ " execute(['pip', 'install', 'pycloudflared'])\n",
+ "\n",
+ " from pycloudflared import try_cloudflare\n",
+ " TUNNEL_URL = try_cloudflare(port=7860).tunnel\n",
+ "\n",
+ " elif tunnel == 'ngrok':\n",
+ " if not has_python_package('pyngrok'):\n",
+ " execute(['pip', 'install', 'pyngrok'])\n",
+ "\n",
+ " auth = None\n",
+ " token = OPTIONS['NGROK_API_TOKEN']\n",
+ "\n",
+ " if ':' in token:\n",
+ " parts = token.split(':')\n",
+ " auth = parts[1] + ':' + parts[-1]\n",
+ " token = parts[0]\n",
+ "\n",
+ " from pyngrok import conf, exception, ngrok, process\n",
+ "\n",
+ " # 로컬 포트가 닫혀있으면 경고 메세지가 스팸마냥 출력되므로 오류만 표시되게 수정함\n",
+ " process.ngrok_logger.setLevel('ERROR')\n",
+ "\n",
+ " try:\n",
+ " tunnel = ngrok.connect(\n",
+ " 7860,\n",
+ " pyngrok_config=conf.PyngrokConfig(\n",
+ " auth_token=token,\n",
+ " region='jp'\n",
+ " ),\n",
+ " auth=auth,\n",
+ " bind_tls=True\n",
+ " )\n",
+ " except exception.PyngrokNgrokError:\n",
+ " alert('ngrok 연결에 실패했습니다, 토큰을 확인해주세요!', True)\n",
+ " else:\n",
+ " assert isinstance(tunnel, ngrok.NgrokTunnel)\n",
+ " TUNNEL_URL = tunnel.public_url\n",
+ "\n",
+ " else:\n",
+ " raise ValueError(f'{tunnel} 에 대응하는 터널 서비스가 존재하지 않습니다')\n",
+ "\n",
+ "\n",
+ "def setup_environment():\n",
+ " # 노트북 환경이라면 로그 표시를 위한 HTML 요소 만들기\n",
+ " if IN_INTERACTIVE:\n",
+ " try:\n",
+ " from IPython.display import display\n",
+ " from ipywidgets import widgets\n",
+ "\n",
+ " global LOG_WIDGET\n",
+ " LOG_WIDGET = widgets.HTML()\n",
+ " display(LOG_WIDGET)\n",
+ "\n",
+ " except ImportError:\n",
+ " pass\n",
+ "\n",
+ " # 구글 드라이브 마운트하기\n",
+ " if IN_COLAB and OPTIONS['USE_GOOGLE_DRIVE']:\n",
+ " from google.colab import drive\n",
+ " drive.mount('/content/drive')\n",
+ "\n",
+ " global WORKSPACE\n",
+ " WORKSPACE = str(\n",
+ " Path('drive', 'MyDrive', WORKSPACE).resolve()\n",
+ " )\n",
+ "\n",
+ " # 로그 파일 만들기\n",
+ " global LOG_FILE\n",
+ " workspace = Path(WORKSPACE).resolve()\n",
+ " log_path = workspace.joinpath(\n",
+ " 'logs',\n",
+ " datetime.strftime(datetime.now(), '%Y-%m-%d_%H-%M-%S.log')\n",
+ " )\n",
+ "\n",
+ " log_path.parent.mkdir(0o777, True, True)\n",
+ "\n",
+ " LOG_FILE = log_path.open('a')\n",
+ "\n",
+ " # 현재 환경 출력\n",
+ " import platform\n",
+ " log(' '.join(os.uname()))\n",
+ " log(f'Python {platform.python_version()}')\n",
+ " log(str(Path().resolve()))\n",
+ "\n",
+ " # 덮어쓸 설정 파일 가져오기\n",
+ " override_path = workspace.joinpath('override.json')\n",
+ " if override_path.exists():\n",
+ " with override_path.open('r') as file:\n",
+ " override_options = json.loads(file.read())\n",
+ " for key, value in override_options.items():\n",
+ " if key not in OPTIONS:\n",
+ " log(f'{key} 키는 존재하지 않는 설정입니다', styles={'color': 'red'})\n",
+ " continue\n",
+ "\n",
+ " if type(value) != type(OPTIONS[key]):\n",
+ " log(f'{key} 키는 {type(OPTIONS[key]).__name__} 자료형이여만 합니다', styles={\n",
+ " 'color': 'red'})\n",
+ " continue\n",
+ "\n",
+ " OPTIONS[key] = value\n",
+ "\n",
+ " log(f'override.json: {key} = {json.dumps(value)}')\n",
+ "\n",
+ " if IN_COLAB:\n",
+ " # 다른 Python 버전 설치\n",
+ " if OPTIONS['PYTHON_EXECUTABLE'] and not find_executable(OPTIONS['PYTHON_EXECUTABLE']):\n",
+ " execute(['apt', 'install', OPTIONS['PYTHON_EXECUTABLE']])\n",
+ " execute(\n",
+ " f\"curl -sS https://bootstrap.pypa.io/get-pip.py | {OPTIONS['PYTHON_EXECUTABLE']}\"\n",
+ " )\n",
+ "\n",
+ " # 선택한 토치 버전 설치\n",
+ " if 'torch==1.13.1+cu117' in TORCH_VERSION:\n",
+ " execute(['pip', 'install', '-q', '-U', 'torch==1.13.1+cu117', 'torchvision==0.14.1+cu117', 'torchaudio==0.13.1+cu117', 'torchtext==0.14.1', 'torchdata==0.5.1', '--extra-index-url', 'https://download.pytorch.org/whl/cu117'])\n",
+ "\n",
+ " if OPTIONS['USE_XFORMERS']:\n",
+ " execute(['pip', 'install', '-q', '-U', 'xformers==0.0.16rc425'])\n",
+ " \n",
+ " # ddetailer 의존 패키지 미리 설치\n",
+ " if INSTALL_DDETAILER_REQUIREMENTS:\n",
+ " execute(['pip', 'install', '-q', '-U', 'openmim==0.3.7'])\n",
+ " execute(['mim', 'install', '-q', '-U', 'mmcv-full==1.7.1'])\n",
+ " execute(['pip', 'install', '-q', '-U', 'mmdet==2.28.2'])\n",
+ "\n",
+ " elif 'torch==2.0.0+cu118' in TORCH_VERSION:\n",
+ " execute(['pip', 'install', '-q', '-U', 'torch==2.0.0+cu118', 'torchvision==0.15.1+cu118', 'torchaudio==2.0.1+cu118', 'torchtext==0.15.1', 'torchdata==0.6.0', '--extra-index-url', 'https://download.pytorch.org/whl/cu118'])\n",
+ "\n",
+ " if OPTIONS['USE_XFORMERS']:\n",
+ " execute(['pip', 'install', '-q', '-U', 'xformers==0.0.17'])\n",
+ "\n",
+ " # 런타임이 정상적으로 초기화 됐는지 확인하기\n",
+ " try:\n",
+ " import torch\n",
+ " except:\n",
+ " alert('torch 패키지가 잘못됐습니다, 런타임을 다시 실행해주세요!', True)\n",
+ " else:\n",
+ " if not torch.cuda.is_available():\n",
+ " alert('GPU 런타임이 아닙니다, 할당량이 초과 됐을 수도 있습니다!')\n",
+ "\n",
+ " OPTIONS['EXTRA_ARGS'] += [\n",
+ " '--skip-torch-cuda-test',\n",
+ " '--no-half',\n",
+ " '--opt-sub-quad-attention'\n",
+ " ]\n",
+ "\n",
+ " # 외부 터널링 초기화\n",
+ " setup_tunnels()\n",
+ "\n",
+ " # 체크포인트 모델이 존재하지 않는다면 기본 모델 받아오기\n",
+ " if not has_checkpoint():\n",
+ " for file in [\n",
+ " {\n",
+ " 'url': 'https://huggingface.co/gsdf/Counterfeit-V2.5/resolve/main/Counterfeit-V2.5_fp16.safetensors',\n",
+ " 'target': str(workspace.joinpath('models/Stable-diffusion/Counterfeit-V2.5_fp16.safetensors')),\n",
+ " 'summary': '기본 체크포인트 파일을 받아옵니다'\n",
+ " },\n",
+ " {\n",
+ " 'url': 'https://huggingface.co/saltacc/wd-1-4-anime/resolve/main/VAE/kl-f8-anime2.ckpt',\n",
+ " 'target': str(workspace.joinpath('models/VAE/kl-f8-anime2.ckpt')),\n",
+ " 'summary': '기본 VAE 파일을 받아옵니다'\n",
+ " }\n",
+ " ]:\n",
+ " download(**file)\n",
+ "\n",
+ "\n",
+ "# ==============================\n",
+ "# 로그\n",
+ "# ==============================\n",
+ "\n",
+ "\n",
+ "def format_styles(styles: dict) -> str:\n",
+ " return ';'.join(map(lambda kv: ':'.join(kv), styles.items()))\n",
+ "\n",
+ "\n",
+ "def render_log() -> None:\n",
+ " try:\n",
+ " from ipywidgets import widgets\n",
+ " except ImportError:\n",
+ " return\n",
+ "\n",
+ " if not isinstance(LOG_WIDGET, widgets.HTML):\n",
+ " return\n",
+ "\n",
+ " html = f'''
'''\n",
+ "\n",
+ " for block in LOG_BLOCKS:\n",
+ " styles = {\n",
+ " 'display': 'inline-block',\n",
+ " **block['styles']\n",
+ " }\n",
+ " child_styles = {\n",
+ " 'display': 'inline-block',\n",
+ " **block['child_styles']\n",
+ " }\n",
+ "\n",
+ " html += f'
{block[\"msg\"]}\\n'\n",
+ "\n",
+ " if block['max_childs'] is not None and len(block['childs']) > 0:\n",
+ " html += f'
'\n",
+ " html += ''.join(block['childs'][-block['max_childs']:])\n",
+ " html += '
'\n",
+ "\n",
+ " html += '
'\n",
+ "\n",
+ " LOG_WIDGET.value = html\n",
+ "\n",
+ "\n",
+ "def log(\n",
+ " msg: str,\n",
+ " styles={},\n",
+ " newline=True,\n",
+ "\n",
+ " parent=False,\n",
+ " parent_index: Optional[int] = None,\n",
+ " child_styles={\n",
+ " 'padding-left': '1em',\n",
+ " 'color': 'gray'\n",
+ " },\n",
+ " max_childs=0,\n",
+ "\n",
+ " print_to_file=True,\n",
+ " print_to_widget=True\n",
+ ") -> Optional[int]:\n",
+ " # 기록할 내용이 ngrok API 키와 일치한다면 숨기기\n",
+ " # TODO: 더 나은 문자열 검사, 원치 않은 내용이 가려질 수도 있음\n",
+ " if OPTIONS['NGROK_API_TOKEN'] != '':\n",
+ " msg = msg.replace(OPTIONS['NGROK_API_TOKEN'], '**REDACTED**')\n",
+ "\n",
+ " if newline:\n",
+ " msg += '\\n'\n",
+ "\n",
+ " # 파일에 기록하기\n",
+ " if print_to_file and LOG_FILE:\n",
+ " if parent_index and msg.endswith('\\n'):\n",
+ " LOG_FILE.write('\\t')\n",
+ " LOG_FILE.write(msg)\n",
+ " LOG_FILE.flush()\n",
+ "\n",
+ " # 로그 위젯에 기록하기\n",
+ " if print_to_widget and LOG_WIDGET:\n",
+ " # 부모 로그가 없다면 새 블록 만들기\n",
+ " if parent or parent_index is None:\n",
+ " LOG_BLOCKS.append({\n",
+ " 'msg': msg,\n",
+ " 'styles': styles,\n",
+ " 'childs': [],\n",
+ " 'child_styles': child_styles,\n",
+ " 'max_childs': max_childs\n",
+ " })\n",
+ " render_log()\n",
+ " return len(LOG_BLOCKS) - 1\n",
+ "\n",
+ " # 부모 로그가 존재한다면 추가하기\n",
+ " if len(LOG_BLOCKS[parent_index]['childs']) > 100:\n",
+ " LOG_BLOCKS[parent_index]['childs'].pop(0)\n",
+ "\n",
+ " LOG_BLOCKS[parent_index]['childs'].append(msg)\n",
+ " render_log()\n",
+ "\n",
+ " print('\\t' if parent_index else '' + msg, end='')\n",
+ "\n",
+ "\n",
+ "def log_trace() -> None:\n",
+ " import sys\n",
+ " import traceback\n",
+ "\n",
+ " # 스택 가져오기\n",
+ " ex_type, ex_value, ex_traceback = sys.exc_info()\n",
+ "\n",
+ " styles = {}\n",
+ "\n",
+ " # 오류가 존재한다면 메세지 빨간색으로 출력하기\n",
+ " # https://docs.python.org/3/library/sys.html#sys.exc_info\n",
+ " # TODO: 오류 유무 이렇게 확인하면 안될거 같은데 일단 귀찮아서 대충 써둠\n",
+ " if ex_type is not None:\n",
+ " styles = LOG_WIDGET_STYLES['dialog_error']\n",
+ "\n",
+ " parent_index = log(\n",
+ " '오류가 발생했습니다, 디스코드 서버에 보고해주세요',\n",
+ " styles)\n",
+ " assert parent_index\n",
+ "\n",
+ " # 오류가 존재한다면 오류 정보와 스택 트레이스 출력하기\n",
+ " if ex_type is not None:\n",
+ " log(f'{ex_type.__name__}: {ex_value}', parent_index=parent_index)\n",
+ " log(\n",
+ " '\\n'.join(traceback.format_tb(ex_traceback)),\n",
+ " parent_index=parent_index\n",
+ " )\n",
+ "\n",
+ " # 로그 파일이 없으면 보고하지 않기\n",
+ " # TODO: 로그 파일이 존재하지 않을 수가 있나...?\n",
+ " if not LOG_FILE:\n",
+ " log('로그 파일이 존재하지 않습니다, 보고서를 만들지 않습니다')\n",
+ " return\n",
+ "\n",
+ "\n",
+ "def alert(message: str, unassign=False):\n",
+ " log(message)\n",
+ "\n",
+ " if IN_INTERACTIVE:\n",
+ " from IPython.display import display\n",
+ " from ipywidgets import widgets\n",
+ "\n",
+ " display(\n",
+ " widgets.HTML(f'')\n",
+ " )\n",
+ "\n",
+ " if IN_COLAB and unassign:\n",
+ " from google.colab import runtime\n",
+ "\n",
+ " time.sleep(1)\n",
+ " runtime.unassign()\n",
+ "\n",
+ "\n",
+ "# ==============================\n",
+ "# 서브 프로세스\n",
+ "# ==============================\n",
+ "def execute(\n",
+ " args: Union[str, List[str]],\n",
+ " parser: Optional[\n",
+ " Callable[[str], None]\n",
+ " ] = None,\n",
+ " summary: Optional[str] = None,\n",
+ " hide_summary=False,\n",
+ " print_to_file=True,\n",
+ " print_to_widget=True,\n",
+ " **kwargs\n",
+ ") -> Tuple[str, int]:\n",
+ " if isinstance(args, str) and 'shell' not in kwargs:\n",
+ " kwargs['shell'] = True\n",
+ "\n",
+ " # 서브 프로세스 만들기\n",
+ " p = subprocess.Popen(\n",
+ " args,\n",
+ " stdout=subprocess.PIPE,\n",
+ " stderr=subprocess.STDOUT,\n",
+ " encoding='utf-8',\n",
+ " **kwargs)\n",
+ "\n",
+ " # 로그에 시작한 프로세스 정보 출력하기\n",
+ " formatted_args = args if isinstance(args, str) else ' '.join(args)\n",
+ " summary = formatted_args if summary is None else f'{summary}\\n {formatted_args}'\n",
+ "\n",
+ " log_index = log(\n",
+ " f'=> {summary}',\n",
+ " styles={'color': 'yellow'},\n",
+ " max_childs=10)\n",
+ "\n",
+ " output = ''\n",
+ "\n",
+ " # 프로세스 출력 위젯에 리다이렉션하기\n",
+ " while p.poll() is None:\n",
+ " # 출력이 비어있다면 넘어가기\n",
+ " assert p.stdout\n",
+ " line = p.stdout.readline()\n",
+ " if not line:\n",
+ " continue\n",
+ "\n",
+ " # 프로세스 출력 버퍼에 추가하기\n",
+ " output += line\n",
+ "\n",
+ " # 파서 함수 실행하기\n",
+ " if callable(parser):\n",
+ " parser(line)\n",
+ "\n",
+ " # 프로세스 출력 로그하기\n",
+ " log(\n",
+ " line,\n",
+ " newline=False,\n",
+ " parent_index=log_index,\n",
+ " print_to_file=print_to_file,\n",
+ " print_to_widget=print_to_widget)\n",
+ "\n",
+ " # 변수 정리하기\n",
+ " rc = p.poll()\n",
+ " assert rc is not None\n",
+ "\n",
+ " # 로그 블록 업데이트\n",
+ " if LOG_WIDGET:\n",
+ " assert log_index\n",
+ "\n",
+ " if rc == 0:\n",
+ " # 현재 로그 텍스트 초록색으로 변경하고 프로세스 출력 숨기기\n",
+ " LOG_BLOCKS[log_index]['styles']['color'] = 'green'\n",
+ " LOG_BLOCKS[log_index]['max_childs'] = None\n",
+ " else:\n",
+ " # 현재 로그 텍스트 빨간색으로 변경하고 프로세스 출력 모두 표시하기\n",
+ " LOG_BLOCKS[log_index]['styles']['color'] = 'red'\n",
+ " LOG_BLOCKS[log_index]['max_childs'] = 0\n",
+ "\n",
+ " if hide_summary:\n",
+ " # 현재 로그 블록 숨기기 (제거하기)\n",
+ " del LOG_BLOCKS[log_index]\n",
+ "\n",
+ " # 로그 블록 렌더링\n",
+ " render_log()\n",
+ "\n",
+ " # 오류 코드를 반환했다면\n",
+ " if rc != 0:\n",
+ " if isinstance(rc, signal.Signals):\n",
+ " rc = rc.value\n",
+ "\n",
+ " raise subprocess.CalledProcessError(rc, args)\n",
+ "\n",
+ " return output, rc\n",
+ "\n",
+ "# ==============================\n",
+ "# 작업 경로\n",
+ "# ==============================\n",
+ "\n",
+ "\n",
+ "def delete(path: os.PathLike) -> None:\n",
+ " path = Path(path)\n",
+ "\n",
+ " if path.is_file() or path.is_symlink():\n",
+ " path.unlink()\n",
+ " else:\n",
+ " shutil.rmtree(path, ignore_errors=True)\n",
+ "\n",
+ "\n",
+ "def has_python_package(pkg: str, executable: Optional[str] = None) -> bool:\n",
+ " if not executable:\n",
+ " return find_spec(pkg) is not None\n",
+ "\n",
+ " _, rc = execute(\n",
+ " [\n",
+ " executable, '-c',\n",
+ " f'''\n",
+ " import importlib\n",
+ " import sys\n",
+ " sys.exit(0 if importlib.find_loader({shlex.quote(pkg)}) else 0)\n",
+ " '''\n",
+ " ])\n",
+ "\n",
+ " return True if rc == 0 else False\n",
+ "\n",
+ "\n",
+ "# ==============================\n",
+ "# 파일 다운로드\n",
+ "# ==============================\n",
+ "def download(url: str, target: Optional[str] = None, ignore_aria2=False, **kwargs):\n",
+ " if not target:\n",
+ " # TODO: 경로 중 params 제거하기\n",
+ " target = url.split('/')[-1]\n",
+ "\n",
+ " # 파일을 받을 디렉터리 만들기\n",
+ " Path(target).parent.mkdir(0o777, True, True)\n",
+ "\n",
+ " # 빠른 다운로드를 위해 aria2 패키지 설치 시도하기\n",
+ " if not ignore_aria2:\n",
+ " if not find_executable('aria2c') and find_executable('apt'):\n",
+ " execute(['apt', 'install', 'aria2'])\n",
+ "\n",
+ " if find_executable('aria2c'):\n",
+ " p = Path(target)\n",
+ " execute(\n",
+ " [\n",
+ " 'aria2c',\n",
+ " '--continue',\n",
+ " '--always-resume',\n",
+ " '--summary-interval', '10',\n",
+ " '--disk-cache', '64M',\n",
+ " '--min-split-size', '8M',\n",
+ " '--max-concurrent-downloads', '8',\n",
+ " '--max-connection-per-server', '8',\n",
+ " '--max-overall-download-limit', '0',\n",
+ " '--max-download-limit', '0',\n",
+ " '--split', '8',\n",
+ " '--dir', str(p.parent),\n",
+ " '--out', p.name,\n",
+ " url\n",
+ " ],\n",
+ " **kwargs)\n",
+ "\n",
+ " elif find_executable('curl'):\n",
+ " execute(\n",
+ " [\n",
+ " 'curl',\n",
+ " '--location',\n",
+ " '--output', target,\n",
+ " url\n",
+ " ],\n",
+ " **kwargs)\n",
+ "\n",
+ " else:\n",
+ " if 'summary' in kwargs.keys():\n",
+ " log(kwargs.pop('summary'), **kwargs)\n",
+ "\n",
+ " with requests.get(url, stream=True) as res:\n",
+ " res.raise_for_status()\n",
+ "\n",
+ " with open(target, 'wb') as file:\n",
+ " # 받아온 파일 디코딩하기\n",
+ " # https://github.com/psf/requests/issues/2155#issuecomment-50771010\n",
+ " import functools\n",
+ " res.raw.read = functools.partial(\n",
+ " res.raw.read,\n",
+ " decode_content=True)\n",
+ "\n",
+ " # TODO: 파일 길이가 적합한지?\n",
+ " shutil.copyfileobj(res.raw, file, length=16*1024*1024)\n",
+ "\n",
+ "\n",
+ "def has_checkpoint() -> bool:\n",
+ " workspace = Path(WORKSPACE)\n",
+ " for p in workspace.joinpath('models', 'Stable-diffusion').glob('**/*'):\n",
+ " if p.suffix != '.ckpt' and p.suffix != '.safetensors':\n",
+ " continue\n",
+ "\n",
+ " # aria2 로 받다만 파일이면 무시하기\n",
+ " if p.with_suffix(p.suffix + '.aria2c').exists():\n",
+ " continue\n",
+ "\n",
+ " return True\n",
+ " return False\n",
+ "\n",
+ "\n",
+ "def parse_webui_output(line: str) -> None:\n",
+ " # 첫 시작에 한해서 웹 서버 열렸을 때 다이어로그 표시하기\n",
+ " if 'Running on local URL:' in line:\n",
+ " log(\n",
+ " '\\n'.join([\n",
+ " '성공적으로 터널이 열렸습니다',\n",
+ " f'{TUNNEL_URL}',\n",
+ " ]),\n",
+ " LOG_WIDGET_STYLES['dialog_success']\n",
+ " )\n",
+ " return\n",
+ "\n",
+ "\n",
+ "def setup_webui() -> None:\n",
+ " repo_dir = Path('repository')\n",
+ " need_clone = True\n",
+ "\n",
+ " # 이미 디렉터리가 존재한다면 정상적인 레포인지 확인하기\n",
+ " if repo_dir.is_dir():\n",
+ " try:\n",
+ " # 사용자 파일만 남겨두고 레포지토리 초기화하기\n",
+ " # https://stackoverflow.com/a/12096327\n",
+ " execute(\n",
+ " 'git stash && git pull origin master',\n",
+ " cwd=repo_dir\n",
+ " )\n",
+ " except subprocess.CalledProcessError:\n",
+ " log('레포지토리가 잘못됐습니다, 디렉터리를 제거합니다')\n",
+ " else:\n",
+ " need_clone = False\n",
+ "\n",
+ " # 레포지토리 클론이 필요하다면 기존 디렉터리 지우고 클론하기\n",
+ " if need_clone:\n",
+ " shutil.rmtree(repo_dir, ignore_errors=True)\n",
+ " execute(['git', 'clone', OPTIONS['REPO_URL'], str(repo_dir)])\n",
+ "\n",
+ " # 특정 커밋이 지정됐다면 체크아웃하기\n",
+ " if OPTIONS['REPO_COMMIT']:\n",
+ " execute(\n",
+ " ['git', 'checkout', OPTIONS['REPO_COMMIT']],\n",
+ " cwd=repo_dir\n",
+ " )\n",
+ "\n",
+ " if IN_COLAB:\n",
+ " patch_path = repo_dir.joinpath('scripts', 'patches.py')\n",
+ "\n",
+ " if not patch_path.exists():\n",
+ " download(\n",
+ " 'https://raw.githubusercontent.com/toriato/easy-stable-diffusion/main/scripts/patches.py',\n",
+ " str(patch_path),\n",
+ " ignore_aria2=True)\n",
+ "\n",
+ "\n",
+ "def start_webui(args: List[str] = OPTIONS['ARGS']) -> None:\n",
+ " workspace = Path(WORKSPACE).resolve()\n",
+ " repository = Path('repository').resolve()\n",
+ "\n",
+ " # 기본 인자 만들기\n",
+ " if len(args) < 1:\n",
+ " args += ['--data-dir', str(workspace)]\n",
+ "\n",
+ " # xformers\n",
+ " if OPTIONS['USE_XFORMERS']:\n",
+ " try:\n",
+ " import torch\n",
+ " except ImportError:\n",
+ " pass\n",
+ " else:\n",
+ " if torch.cuda.is_available():\n",
+ " args += [\n",
+ " '--xformers',\n",
+ " '--xformers-flash-attention'\n",
+ " ]\n",
+ "\n",
+ " # Gradio 인증 정보\n",
+ " if OPTIONS['GRADIO_USERNAME'] != '':\n",
+ " args += [\n",
+ " f'--gradio-auth',\n",
+ " OPTIONS['GRADIO_USERNAME'] +\n",
+ " ('' if OPTIONS['GRADIO_PASSWORD'] ==\n",
+ " '' else ':' + OPTIONS['GRADIO_PASSWORD'])\n",
+ " ]\n",
+ "\n",
+ " # 추가 인자\n",
+ " args += OPTIONS['EXTRA_ARGS']\n",
+ "\n",
+ " env = {\n",
+ " **os.environ,\n",
+ " 'HF_HOME': str(workspace / 'cache' / 'huggingface')\n",
+ " }\n",
+ "\n",
+ " try:\n",
+ " execute(\n",
+ " [\n",
+ " OPTIONS['PYTHON_EXECUTABLE'] or 'python',\n",
+ " '-u',\n",
+ " '-m', 'launch',\n",
+ " *args\n",
+ " ],\n",
+ " parser=parse_webui_output,\n",
+ " cwd=str(repository),\n",
+ " env=env,\n",
+ " start_new_session=True,\n",
+ " )\n",
+ " except subprocess.CalledProcessError as e:\n",
+ " if IN_COLAB and e.returncode == signal.SIGINT.value:\n",
+ " raise RuntimeError(\n",
+ " '프로세스가 강제 종료됐습니다, 메모리가 부족해 발생한 문제일 수도 있습니다') from e\n",
+ "\n",
+ "\n",
+ "try:\n",
+ " setup_environment()\n",
+ "\n",
+ " # 3단 이상(?) 레벨에서 실행하면 nested 된 asyncio 이 문제를 일으킴\n",
+ " # 런타임을 종료해도 코랩 페이지에선 런타임이 실행 중(Busy)인 것으로 표시되므로 여기서 실행함\n",
+ " if OPTIONS['DISCONNECT_RUNTIME']:\n",
+ " hook_runtime_disconnect()\n",
+ "\n",
+ " setup_webui()\n",
+ " start_webui()\n",
+ "\n",
+ "# ^c 종료 무시하기\n",
+ "except KeyboardInterrupt:\n",
+ " pass\n",
+ "\n",
+ "except:\n",
+ " # 로그 위젯이 없다면 평범하게 오류 처리하기\n",
+ " if not LOG_WIDGET:\n",
+ " raise\n",
+ "\n",
+ " log_trace()\n"
+ ],
+ "metadata": {
+ "id": "UGSqtUJPJoOj",
+ "cellView": "form"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "
\n",
+ "
\n",
+ "\n",
+ "
\n",
+ "
\n",
+ "## 🔨 /dev/stuffs"
+ ],
+ "metadata": {
+ "id": "68jj0NEN7w2b"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### 좀비 프로세스 죽이기"
+ ],
+ "metadata": {
+ "id": "YNylYhg2KzJl"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "#@title\n",
+ "!kill -9 $(pgrep -f '\\-m launch')\n",
+ "!free -h"
+ ],
+ "metadata": {
+ "cellView": "form",
+ "id": "VXz1XdVh1wtC"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### 풀 리퀘스트 적용하기\n",
+ "https://github.com/AUTOMATIC1111/stable-diffusion-webui/pulls\n",
+ "\n"
+ ],
+ "metadata": {
+ "id": "YLYoxXP8K4jb"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "#@title\n",
+ "_PR_ID = 7710 #@param {type:\"integer\"}\n",
+ "!(cd repository && git fetch origin pull/7710/head && git checkout FETCH_HEAD)"
+ ],
+ "metadata": {
+ "cellView": "form",
+ "id": "mwv_y29E1AoJ"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### 모델 다운로드\n"
+ ],
+ "metadata": {
+ "id": "cfFYv1jw-L2b"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "from google.colab import drive\n",
+ "drive.mount('/content/drive')\n",
+ "!apt -qq install -y aria2"
+ ],
+ "metadata": {
+ "id": "tHBZa2MA-4sz"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "!aria2c --out wd15-beta1-fp16.safetensors https://huggingface.co/waifu-diffusion/wd-1-5-beta/resolve/main/checkpoints/wd15-beta1-fp16.safetensors\n",
+ "!aria2c --out wd15-beta1-fp16.yaml https://huggingface.co/waifu-diffusion/wd-1-5-beta/resolve/main/checkpoints/wd15-beta1-fp16.yaml"
+ ],
+ "metadata": {
+ "id": "EdT0sp8R-Qu8"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "!mv wd15-beta1-fp16* /content/drive/MyDrive/SD/models/Stable-diffusion"
+ ],
+ "metadata": {
+ "id": "bXKRvS9--3Xp"
+ },
+ "execution_count": null,
+ "outputs": []
+ }
+ ],
+ "metadata": {
+ "colab": {
+ "provenance": [],
+ "private_outputs": true,
+ "collapsed_sections": [
+ "YNylYhg2KzJl",
+ "YLYoxXP8K4jb",
+ "cfFYv1jw-L2b"
+ ]
+ },
+ "kernelspec": {
+ "display_name": "Python 3",
+ "name": "python3"
+ },
+ "language_info": {
+ "name": "python"
+ },
+ "gpuClass": "standard",
+ "accelerator": "GPU"
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
\ No newline at end of file
diff --git a/notebooks/easy_stable_diffusion_downloader.ipynb b/notebooks/easy_stable_diffusion_downloader.ipynb
new file mode 100644
index 0000000..ac5800e
--- /dev/null
+++ b/notebooks/easy_stable_diffusion_downloader.ipynb
@@ -0,0 +1,738 @@
+{
+ "nbformat": 4,
+ "nbformat_minor": 0,
+ "metadata": {
+ "colab": {
+ "private_outputs": true,
+ "provenance": []
+ },
+ "kernelspec": {
+ "name": "python3",
+ "display_name": "Python 3"
+ },
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## 실행하는 방법\n",
+ "\n",
+ "- [버그 보고 및 질답용 디스코드 서버](https://discord.gg/6wQeA2QXgM)\n",
+ "- [원클릭 코랩 노트북](https://colab.research.google.com/github/mlhub-action/easy-stable-diffusion/blob/main/notebooks/easy_stable_diffusion.ipynb)\n",
+ "- [원클릭 다운로더 코랩 노트북 (최신 버전)](https://colab.research.google.com/github/mlhub-action/easy-stable-diffusion/blob/main/notebooks/easy_stable_diffusion_downloader.ipynb)\n",
+ "- [레포지토리 (최신 버전)](https://github.com/mlhub-action/easy-stable-diffusion)\n",
+ "\n",
+ "1. 아래에 있는 **실행** 셀 좌측에 있는 있는 **재생(▶) 아이콘** 클릭 ([예시 이미지](https://cdn.discordapp.com/attachments/872959812407816235/1029512475781103757/2022-10-12_06-55-01_02b9_librewolf.png))\n",
+ "1. 원하는 모델 선택 후 **다운로드 버튼** 누르고 대기"
+ ],
+ "metadata": {
+ "id": "6bXO2y4vG47x"
+ }
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "cellView": "form",
+ "id": "MbsTXP8fqoYg"
+ },
+ "outputs": [],
+ "source": [
+ "import os\n",
+ "import shlex\n",
+ "import time\n",
+ "\n",
+ "from pathlib import Path\n",
+ "from urllib.parse import urlparse, unquote\n",
+ "from tempfile import TemporaryDirectory\n",
+ "from typing import Union, List, Dict\n",
+ "from IPython.display import display\n",
+ "from ipywidgets import widgets\n",
+ "from google.colab import drive, runtime\n",
+ "\n",
+ "# fmt: off\n",
+ "#@title\n",
+ "\n",
+ "#@markdown ### ***작업 디렉터리 경로***\n",
+ "#@markdown 모델 파일 등이 영구적으로 보관될 디렉터리 경로\n",
+ "WORKSPACE = 'SD' #@param {type:\"string\"}\n",
+ "\n",
+ "#@markdown ##### ***다운로드가 끝나면 자동으로 코랩 런타임을 종료할지?***\n",
+ "DISCONNECT_RUNTIME = True #@param {type:\"boolean\"}\n",
+ "\n",
+ "# fmt: on\n",
+ "\n",
+ "# 인터페이스 요소\n",
+ "dropdowns = widgets.VBox()\n",
+ "output = widgets.Output()\n",
+ "download_button = widgets.Button(\n",
+ " description='다운로드',\n",
+ " disabled=True,\n",
+ " layout={\"width\": \"99%\"}\n",
+ ")\n",
+ "\n",
+ "display(\n",
+ " widgets.HBox(children=(\n",
+ " widgets.VBox(\n",
+ " children=(dropdowns, download_button),\n",
+ " layout={\"margin-right\": \"1em\"}\n",
+ " ),\n",
+ " output\n",
+ " )))\n",
+ "\n",
+ "\n",
+ "# 파일 경로\n",
+ "workspace_dir = Path('drive', 'MyDrive', WORKSPACE)\n",
+ "sd_model_dir = workspace_dir.joinpath('models', 'Stable-diffusion')\n",
+ "sd_embedding_dir = workspace_dir.joinpath('embeddings')\n",
+ "vae_dir = workspace_dir.joinpath('models', 'VAE')\n",
+ "\n",
+ "# 구글 드라이브 마운팅\n",
+ "with output:\n",
+ " drive.mount('drive')\n",
+ "\n",
+ "sd_model_dir.mkdir(0o777, True, True)\n",
+ "sd_embedding_dir.mkdir(0o777, True, True)\n",
+ "vae_dir.mkdir(0o777, True, True)\n",
+ "\n",
+ "\n",
+ "class File:\n",
+ " prefix: Path\n",
+ "\n",
+ " def __init__(self, url: str, path: os.PathLike = None, *extra_args: List[str]) -> None:\n",
+ " if self.prefix:\n",
+ " if not path:\n",
+ " path = self.prefix\n",
+ " elif type(path) == str:\n",
+ " path = self.prefix.joinpath(path)\n",
+ "\n",
+ " self.url = url\n",
+ " self.path = Path(path)\n",
+ " self.extra_args = extra_args\n",
+ "\n",
+ " def download(self) -> None:\n",
+ " output.clear_output()\n",
+ "\n",
+ " with TemporaryDirectory() as tempdir:\n",
+ " args = shlex.join((\n",
+ " '--continue',\n",
+ " '--always-resume',\n",
+ " '--summary-interval', '3',\n",
+ " '--console-log-level', 'error',\n",
+ " '--max-concurrent-downloads', '16',\n",
+ " '--max-connection-per-server', '16',\n",
+ " '--split', '16',\n",
+ " '--dir', tempdir,\n",
+ " *self.extra_args,\n",
+ " self.url\n",
+ " ))\n",
+ "\n",
+ " with output:\n",
+ " # aria2 로 파일 받아오기\n",
+ " # fmt: off\n",
+ " !which aria2c || apt install -y aria2\n",
+ " output.clear_output()\n",
+ "\n",
+ " print('aria2 를 사용해 파일을 받아옵니다.')\n",
+ " !aria2c {args}\n",
+ " output.clear_output()\n",
+ "\n",
+ " print('파일을 성공적으로 받았습니다, 드라이브로 이동합니다.')\n",
+ " print('이 작업은 파일의 크기에 따라 5분 이상 걸릴 수도 있으니 잠시만 기다려주세요.')\n",
+ " if DISCONNECT_RUNTIME:\n",
+ " print('작업이 완료되면 런타임을 자동으로 해제하니 다른 작업을 진행하셔도 좋습니다.')\n",
+ "\n",
+ " # 목적지 경로가 디렉터리가 아니라면 그대로 사용하기\n",
+ " filename = str(self.path) if not self.path.is_dir() else self.path.joinpath(\n",
+ " # 아니라면 파일 원격 주소로부터 파일 이름 가져오기\n",
+ " unquote(os.path.basename(urlparse(self.url).path))\n",
+ " )\n",
+ "\n",
+ " print(f'경로: {filename}')\n",
+ "\n",
+ " !rsync -aP \"{tempdir}/$(ls -AU {tempdir} | head -1)\" \"{filename}\"\n",
+ "\n",
+ " # fmt: on\n",
+ "\n",
+ "\n",
+ "class ModelFile(File):\n",
+ " prefix = sd_model_dir\n",
+ "\n",
+ "\n",
+ "class EmbeddingFile(File):\n",
+ " prefix = sd_embedding_dir\n",
+ "\n",
+ "\n",
+ "class VaeFile(File):\n",
+ " prefix = vae_dir\n",
+ "\n",
+ "\n",
+ "# 모델 목록\n",
+ "CONFIG_V2_V = 'https://raw.githubusercontent.com/Stability-AI/stablediffusion/main/configs/stable-diffusion/v2-inference-v.yaml'\n",
+ "\n",
+ "files = {\n",
+ " 'Stable-Diffusion Checkpoints': {\n",
+ " # 현재 목록의 키 값 정렬해서 보여주기\n",
+ " '$sort': True,\n",
+ "\n",
+ " 'Stable Diffusion': {\n",
+ " 'v2.1': {\n",
+ " '768-v': {\n",
+ " 'ema-pruned': {\n",
+ " 'safetensors': [\n",
+ " ModelFile(\n",
+ " 'https://huggingface.co/stabilityai/stable-diffusion-2-1/resolve/main/v2-1_768-ema-pruned.safetensors',\n",
+ " 'stable-diffusion-v2-1-786-v-ema-pruned.safetensors'),\n",
+ " ModelFile(\n",
+ " CONFIG_V2_V, 'stable-diffusion-v2-1-786-v-ema-pruned.yaml'),\n",
+ " ],\n",
+ " 'ckpt': [\n",
+ " ModelFile(\n",
+ " 'https://huggingface.co/stabilityai/stable-diffusion-2-1/resolve/main/v2-1_768-ema-pruned.ckpt',\n",
+ " 'stable-diffusion-v2-1-786-v-ema-pruned.ckpt'),\n",
+ " ModelFile(\n",
+ " CONFIG_V2_V, 'stable-diffusion-v2-1-786-v-ema-pruned.yaml'),\n",
+ " ]\n",
+ " },\n",
+ " 'nonema-pruned': {\n",
+ " 'safetensors': [\n",
+ " ModelFile(\n",
+ " 'https://huggingface.co/stabilityai/stable-diffusion-2-1/resolve/main/v2-1_768-nonema-pruned.safetensors',\n",
+ " 'stable-diffusion-v2-1-786-v-nonema-pruned.safetensors'),\n",
+ " ModelFile(\n",
+ " CONFIG_V2_V, 'stable-diffusion-v2-1-786-v-ema-pruned.yaml'),\n",
+ " ],\n",
+ " 'ckpt': [\n",
+ " ModelFile(\n",
+ " 'https://huggingface.co/stabilityai/stable-diffusion-2-1/resolve/main/v2-1_768-nonema-pruned.ckpt',\n",
+ " 'stable-diffusion-v2-1-786-v-nonema-pruned.ckpt'),\n",
+ " ModelFile(\n",
+ " CONFIG_V2_V, 'stable-diffusion-v2-1-786-v-ema-pruned.yaml'),\n",
+ " ],\n",
+ " }\n",
+ " },\n",
+ " '512-base': {\n",
+ " 'ema-pruned': {\n",
+ " 'safetensors': ModelFile(\n",
+ " 'https://huggingface.co/stabilityai/stable-diffusion-2-1-base/resolve/main/v2-1_512-ema-pruned.safetensors',\n",
+ " 'stable-diffusion-v2-1-512-base-ema-pruned.safetensors'),\n",
+ " 'ckpt': ModelFile(\n",
+ " 'https://huggingface.co/stabilityai/stable-diffusion-2-1-base/resolve/main/v2-1_512-ema-pruned.ckpt',\n",
+ " 'stable-diffusion-v2-1-512-base-ema-pruned.ckpt'),\n",
+ " },\n",
+ " 'nonema-pruned': {\n",
+ " 'safetensors': ModelFile(\n",
+ " 'https://huggingface.co/stabilityai/stable-diffusion-2-1-base/resolve/main/v2-1_512-nonema-pruned.safetensors',\n",
+ " 'stable-diffusion-v2-1-512-base-nonema-pruned.safetensors'),\n",
+ " 'ckpt': ModelFile(\n",
+ " 'https://huggingface.co/stabilityai/stable-diffusion-2-1-base/resolve/main/v2-1_512-nonema-pruned.ckpt',\n",
+ " 'stable-diffusion-v2-1-512-base-nonema-pruned.ckpt'),\n",
+ " },\n",
+ " },\n",
+ " },\n",
+ " 'v2.0': {\n",
+ " '768-v-ema': {\n",
+ " 'safetensors': [\n",
+ " ModelFile(\n",
+ " 'https://huggingface.co/stabilityai/stable-diffusion-2/resolve/main/768-v-ema.safetensors',\n",
+ " 'stable-diffusion-v2-0-786-v-ema.safetensors'),\n",
+ " ModelFile(\n",
+ " CONFIG_V2_V, 'stable-diffusion-v2-1-786-v-ema-pruned.yaml'),\n",
+ " ],\n",
+ " 'ckpt': [\n",
+ " ModelFile(\n",
+ " 'https://huggingface.co/stabilityai/stable-diffusion-2/resolve/main/768-v-ema.ckpt',\n",
+ " 'stable-diffusion-v2-0-786-v-ema.ckpt'),\n",
+ " ModelFile(\n",
+ " CONFIG_V2_V, 'stable-diffusion-v2-1-786-v-ema-pruned.yaml'),\n",
+ " ],\n",
+ " },\n",
+ " '512-base-ema': {\n",
+ " 'safetensors': ModelFile(\n",
+ " 'https://huggingface.co/stabilityai/stable-diffusion-2-base/resolve/main/512-base-ema.safetensors',\n",
+ " 'stable-diffusion-v2-0-512-base-ema.safetensors'),\n",
+ " 'ckpt': ModelFile(\n",
+ " 'https://huggingface.co/stabilityai/stable-diffusion-2-base/resolve/main/512-base-ema.ckpt',\n",
+ " 'stable-diffusion-v2-0-512-base-ema.ckpt'),\n",
+ " },\n",
+ " },\n",
+ " 'v1.5': {\n",
+ " 'pruned-emaonly': {\n",
+ " 'ckpt': ModelFile(\n",
+ " 'https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.ckpt',\n",
+ " 'stable-diffusion-v1-5-pruned-emaonly.ckpt')\n",
+ " },\n",
+ " 'pruned': {\n",
+ " 'ckpt': ModelFile(\n",
+ " 'https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned.ckpt',\n",
+ " 'stable-diffusion-v1-5-pruned.ckpt')\n",
+ " },\n",
+ " },\n",
+ " },\n",
+ "\n",
+ " 'Dreamlike': {\n",
+ " 'photoreal': {\n",
+ " 'v2.0': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/dreamlike-art/dreamlike-photoreal-2.0/resolve/main/dreamlike-photoreal-2.0.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/dreamlike-art/dreamlike-photoreal-2.0/resolve/main/dreamlike-photoreal-2.0.ckpt')\n",
+ " },\n",
+ " 'v1.0': {\n",
+ " 'ckpt': ModelFile('https://huggingface.co/dreamlike-art/dreamlike-photoreal-1.0/resolve/main/dreamlike-photoreal-1.0.ckpt')\n",
+ " },\n",
+ " },\n",
+ " 'diffusion': {\n",
+ " 'v1.0': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/dreamlike-art/dreamlike-diffusion-1.0/resolve/main/dreamlike-diffusion-1.0.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/dreamlike-art/dreamlike-diffusion-1.0/resolve/main/dreamlike-diffusion-1.0.ckpt')\n",
+ " },\n",
+ " }\n",
+ " },\n",
+ "\n",
+ " 'Waifu Diffusion': {\n",
+ " 'v1.4': {\n",
+ " 'anime': {\n",
+ " 'e2': {\n",
+ " 'fp16': {\n",
+ " 'safetensors': [\n",
+ " ModelFile(\n",
+ " 'https://huggingface.co/saltacc/wd-1-4-anime/resolve/main/wd-1-4-epoch2-fp16.safetensors'),\n",
+ " ModelFile(\n",
+ " 'https://huggingface.co/saltacc/wd-1-4-anime/resolve/main/wd-1-4-epoch2-fp16.yaml')\n",
+ " ],\n",
+ " 'ckpt': [\n",
+ " ModelFile(\n",
+ " 'https://huggingface.co/saltacc/wd-1-4-anime/resolve/main/wd-1-4-epoch2-fp16.ckpt'),\n",
+ " ModelFile(\n",
+ " 'https://huggingface.co/saltacc/wd-1-4-anime/resolve/main/wd-1-4-epoch2-fp16.yaml')\n",
+ " ]\n",
+ " },\n",
+ " 'fp32': {\n",
+ " 'safetensors': [\n",
+ " ModelFile(\n",
+ " 'https://huggingface.co/saltacc/wd-1-4-anime/resolve/main/wd-1-4-epoch2-fp32.safetensors'),\n",
+ " ModelFile(\n",
+ " 'https://huggingface.co/saltacc/wd-1-4-anime/resolve/main/wd-1-4-epoch2-fp32.yaml')\n",
+ " ],\n",
+ " 'ckpt': [\n",
+ " ModelFile(\n",
+ " 'https://huggingface.co/saltacc/wd-1-4-anime/resolve/main/wd-1-4-epoch2-fp32.ckpt'),\n",
+ " ModelFile(\n",
+ " 'https://huggingface.co/saltacc/wd-1-4-anime/resolve/main/wd-1-4-epoch2-fp32.yaml')\n",
+ " ]\n",
+ " },\n",
+ " },\n",
+ " 'e1': {\n",
+ " 'ckpt': [\n",
+ " ModelFile(\n",
+ " 'https://huggingface.co/hakurei/waifu-diffusion-v1-4/resolve/main/wd-1-4-anime_e1.ckpt'),\n",
+ " ModelFile(\n",
+ " 'https://huggingface.co/hakurei/waifu-diffusion-v1-4/resolve/main/wd-1-4-anime_e1.yaml'),\n",
+ " ]\n",
+ " },\n",
+ " },\n",
+ " 'booru-step-14000-unofficial': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/waifu-diffusion/unofficial-releases/resolve/main/wd14-booru-step-14000-unofficial.safetensors'),\n",
+ " },\n",
+ " },\n",
+ " 'v1.3.5': {\n",
+ " '80000-fp32': {\n",
+ " 'ckpt': ModelFile('https://huggingface.co/hakurei/waifu-diffusion-v1-4/resolve/main/models/wd-1-3-5_80000-fp32.ckpt'),\n",
+ " },\n",
+ " 'penultimate-ucg-cont': {\n",
+ " 'ckpt': ModelFile('https://huggingface.co/hakurei/waifu-diffusion-v1-4/resolve/main/models/wd-1-3-penultimate-ucg-cont.ckpt'),\n",
+ " }\n",
+ " },\n",
+ " 'v1.3': {\n",
+ " 'fp16': {\n",
+ " 'ckpt': ModelFile('https://huggingface.co/hakurei/waifu-diffusion-v1-3/resolve/main/wd-v1-3-float16.ckpt')\n",
+ " },\n",
+ " 'fp32': {\n",
+ " 'ckpt': ModelFile('https://huggingface.co/hakurei/waifu-diffusion-v1-3/resolve/main/wd-v1-3-float32.ckpt')\n",
+ " },\n",
+ " 'full': {\n",
+ " 'ckpt': ModelFile('https://huggingface.co/hakurei/waifu-diffusion-v1-3/resolve/main/wd-v1-3-full.ckpt')\n",
+ " },\n",
+ " 'full-opt': {\n",
+ " 'ckpt': ModelFile('https://huggingface.co/hakurei/waifu-diffusion-v1-3/resolve/main/wd-v1-3-full-opt.ckpt')\n",
+ " },\n",
+ " },\n",
+ " },\n",
+ "\n",
+ " 'TrinArt': {\n",
+ " 'derrida_characters': {\n",
+ " 'v2': {\n",
+ " 'final': {\n",
+ " 'ckpt': ModelFile(\n",
+ " 'https://huggingface.co/naclbit/trinart_derrida_characters_v2_stable_diffusion/resolve/main/derrida_final.ckpt',\n",
+ " 'trinart_characters_v2_final.ckpt')\n",
+ " },\n",
+ " },\n",
+ " 'v1 (19.2m)': {\n",
+ " 'ckpt': ModelFile('https://huggingface.co/naclbit/trinart_characters_19.2m_stable_diffusion_v1/resolve/main/trinart_characters_it4_v1.ckpt')\n",
+ " },\n",
+ " },\n",
+ " 'v2': {\n",
+ " '115000': {\n",
+ " 'ckpt': ModelFile('https://huggingface.co/naclbit/trinart_stable_diffusion_v2/resolve/main/trinart2_step115000.ckpt'),\n",
+ " },\n",
+ " '95000': {\n",
+ " 'ckpt': ModelFile('https://huggingface.co/naclbit/trinart_stable_diffusion_v2/resolve/main/trinart2_step95000.ckpt'),\n",
+ " },\n",
+ " '60000': {\n",
+ " 'ckpt': ModelFile('https://huggingface.co/naclbit/trinart_stable_diffusion_v2/resolve/main/trinart2_step60000.ckpt'),\n",
+ " },\n",
+ " },\n",
+ " },\n",
+ "\n",
+ " 'AniReal': {\n",
+ " 'v1.0': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/Hosioka/AniReal/resolve/main/AniReal.safetensors')\n",
+ " }\n",
+ " },\n",
+ "\n",
+ " 'OrangeMixs': {\n",
+ " 'AbyssOrangeMix': {\n",
+ " '2': {\n",
+ " 'hard': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix2/AbyssOrangeMix2_hard.safetensors'),\n",
+ " },\n",
+ " 'nsfw': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix2/AbyssOrangeMix2_nsfw.safetensors'),\n",
+ " },\n",
+ " 'sfw': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix2/AbyssOrangeMix2_sfw.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix2/AbyssOrangeMix2_sfw.ckpt')\n",
+ " }\n",
+ " },\n",
+ " '1': {\n",
+ " 'half': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix/AbyssOrangeMix_half.safetensors'),\n",
+ " },\n",
+ " 'night': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix/AbyssOrangeMix_Night.safetensors'),\n",
+ " },\n",
+ " 'base': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix/AbyssOrangeMix.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix/AbyssOrangeMix_base.ckpt'),\n",
+ " },\n",
+ " }\n",
+ " },\n",
+ " 'EerieOrangeMix': {\n",
+ " '2': {\n",
+ " 'half': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/EerieOrangeMix/EerieOrangeMix2_half.safetensors'),\n",
+ " },\n",
+ " 'night': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/EerieOrangeMix/EerieOrangeMix2_night.safetensors'),\n",
+ " },\n",
+ " 'base': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/EerieOrangeMix/EerieOrangeMix2.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/EerieOrangeMix/EerieOrangeMix2_base.ckpt'),\n",
+ " }\n",
+ " },\n",
+ " '1': {\n",
+ " 'half': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/EerieOrangeMix/EerieOrangeMix_half.safetensors'),\n",
+ " },\n",
+ " 'night': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/EerieOrangeMix/EerieOrangeMix_night.safetensors'),\n",
+ " },\n",
+ " 'base': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/EerieOrangeMix/EerieOrangeMix.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/EerieOrangeMix/EerieOrangeMix_base.ckpt'),\n",
+ " }\n",
+ " },\n",
+ " },\n",
+ " },\n",
+ "\n",
+ " 'Anything': {\n",
+ " 'v4.5 (unofficial merge)': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/andite/anything-v4.0/resolve/main/anything-v4.5-pruned.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/andite/anything-v4.0/resolve/main/anything-v4.5-pruned.ckpt'),\n",
+ " },\n",
+ " 'v4.0 (unofficial merge)': {\n",
+ " 'pruned': {\n",
+ " 'fp16': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/andite/anything-v4.0/resolve/main/anything-v4.0-pruned-fp16.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/andite/anything-v4.0/resolve/main/anything-v4.0-pruned-fp16.ckpt'),\n",
+ " },\n",
+ " 'fp32': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/andite/anything-v4.0/resolve/main/anything-v4.0-pruned-fp32.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/andite/anything-v4.0/resolve/main/anything-v4.0-pruned-fp32.ckpt'),\n",
+ " },\n",
+ " 'safetensors': ModelFile('https://huggingface.co/andite/anything-v4.0/resolve/main/anything-v4.0-pruned.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/andite/anything-v4.0/resolve/main/anything-v4.0-pruned.ckpt'),\n",
+ " }\n",
+ " }\n",
+ " },\n",
+ "\n",
+ " 'Protogen': {\n",
+ " 'v8.6 Infinity': {\n",
+ " 'ckpt': ModelFile(\n",
+ " 'https://huggingface.co/darkstorm2150/Protogen_Infinity_Official_Release/resolve/main/model.ckpt',\n",
+ " 'ProtoGen_Infinity.ckpt')\n",
+ " },\n",
+ " 'v8.0 Nova (Experimental)': {\n",
+ " 'ckpt': ModelFile(\n",
+ " 'https://huggingface.co/darkstorm2150/Protogen_Nova_Official_Release/resolve/main/model.ckpt',\n",
+ " 'ProtoGen_Nova.ckpt')\n",
+ " },\n",
+ " 'v7.4 Eclipse (Advanced)': {\n",
+ " 'ckpt': ModelFile(\n",
+ " 'https://huggingface.co/darkstorm2150/Protogen_Eclipse_Official_Release/resolve/main/model.ckpt',\n",
+ " 'ProtoGen_Eclipse.ckpt')\n",
+ " },\n",
+ " 'v5.9 Dragon (RPG themes)': {\n",
+ " 'pruned': {\n",
+ " 'fp16': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/darkstorm2150/Protogen_Dragon_Official_Release/resolve/main/ProtoGen_Dragon-pruned-fp16.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/darkstorm2150/Protogen_Dragon_Official_Release/resolve/main/ProtoGen_Dragon-pruned-fp16.ckpt'),\n",
+ " }\n",
+ " },\n",
+ " 'safetensors': ModelFile('https://huggingface.co/darkstorm2150/Protogen_Dragon_Official_Release/resolve/main/ProtoGen_Dragon.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/darkstorm2150/Protogen_Dragon_Official_Release/resolve/main/ProtoGen_Dragon.ckpt'),\n",
+ " },\n",
+ " 'v5.8 (Sci-Fi/Anime)': {\n",
+ " 'pruned': {\n",
+ " 'fp16': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/darkstorm2150/Protogen_x5.8_Official_Release/resolve/main/ProtoGen_X5.8-pruned-fp16.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/darkstorm2150/Protogen_x5.8_Official_Release/resolve/main/ProtoGen_X5.8-pruned-fp16.ckpt'),\n",
+ " }\n",
+ " },\n",
+ " 'safetensors': ModelFile('https://huggingface.co/darkstorm2150/Protogen_x5.8_Official_Release/resolve/main/ProtoGen_X5.8.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/darkstorm2150/Protogen_x5.8_Official_Release/resolve/main/ProtoGen_X5.8.ckpt'),\n",
+ " },\n",
+ " 'v5.3 (Photorealism)': {\n",
+ " 'pruned': {\n",
+ " 'fp16': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/darkstorm2150/Protogen_x5.3_Official_Release/resolve/main/ProtoGen_X5.3-pruned-fp16.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/darkstorm2150/Protogen_x5.3_Official_Release/resolve/main/ProtoGen_X5.3-pruned-fp16.ckpt'),\n",
+ " }\n",
+ " },\n",
+ " 'safetensors': ModelFile('https://huggingface.co/darkstorm2150/Protogen_x5.3_Official_Release/resolve/main/ProtoGen_X5.3.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/darkstorm2150/Protogen_x5.3_Official_Release/resolve/main/ProtoGen_X5.3.ckpt'),\n",
+ " },\n",
+ " 'v3.4 (Photorealism)': {\n",
+ " 'pruned': {\n",
+ " 'fp16': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/darkstorm2150/Protogen_x3.4_Official_Release/resolve/main/ProtoGen_X3.4-pruned-fp16.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/darkstorm2150/Protogen_x3.4_Official_Release/resolve/main/ProtoGen_X3.4-pruned-fp16.ckpt'),\n",
+ " }\n",
+ " },\n",
+ " 'safetensors': ModelFile('https://huggingface.co/darkstorm2150/Protogen_x3.4_Official_Release/resolve/main/ProtoGen_X3.4.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/darkstorm2150/Protogen_x3.4_Official_Release/resolve/main/ProtoGen_X3.4.ckpt'),\n",
+ " },\n",
+ " 'v2.2 (Anime)': {\n",
+ " 'pruned': {\n",
+ " 'fp16': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/darkstorm2150/Protogen_v2.2_Official_Release/resolve/main/Protogen_V2.2-pruned-fp16.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/darkstorm2150/Protogen_v2.2_Official_Release/resolve/main/Protogen_V2.2-pruned-fp16.ckpt'),\n",
+ " }\n",
+ " },\n",
+ " 'safetensors': ModelFile('https://huggingface.co/darkstorm2150/Protogen_v2.2_Official_Release/resolve/main/Protogen_V2.2.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/darkstorm2150/Protogen_v2.2_Official_Release/resolve/main/Protogen_V2.2.ckpt'),\n",
+ " },\n",
+ " },\n",
+ "\n",
+ " '7th_Layer': {\n",
+ " '7th_anime': {\n",
+ " 'v3.0': {\n",
+ " 'A': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/syaimu/7th_Layer/resolve/main/7th_anime_v3/7th_anime_v3_A.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/syaimu/7th_Layer/resolve/main/7th_anime_v3/7th_anime_v3_A.ckpt'),\n",
+ " },\n",
+ " 'B': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/syaimu/7th_Layer/resolve/main/7th_anime_v3/7th_anime_v3_B.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/syaimu/7th_Layer/resolve/main/7th_anime_v3/7th_anime_v3_B.ckpt'),\n",
+ " },\n",
+ " 'C': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/syaimu/7th_Layer/resolve/main/7th_anime_v3/7th_anime_v3_C.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/syaimu/7th_Layer/resolve/main/7th_anime_v3/7th_anime_v3_C.ckpt'),\n",
+ " },\n",
+ " },\n",
+ " 'v2.0': {\n",
+ " 'A': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/syaimu/7th_Layer/resolve/main/7th_anime_v2/7th_anime_v2_A.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/syaimu/7th_Layer/resolve/main/7th_anime_v2/7th_anime_v2_A.ckpt'),\n",
+ " },\n",
+ " 'B': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/syaimu/7th_Layer/resolve/main/7th_anime_v2/7th_anime_v2_B.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/syaimu/7th_Layer/resolve/main/7th_anime_v2/7th_anime_v2_B.ckpt'),\n",
+ " },\n",
+ " 'C': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/syaimu/7th_Layer/resolve/main/7th_anime_v2/7th_anime_v2_C.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/syaimu/7th_Layer/resolve/main/7th_anime_v2/7th_anime_v2_C.ckpt'),\n",
+ " },\n",
+ " 'G': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/syaimu/7th_Layer/resolve/main/7th_anime_v2/7th_anime_v2_G.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/syaimu/7th_Layer/resolve/main/7th_anime_v2/7th_anime_v2_G.ckpt'),\n",
+ " },\n",
+ " },\n",
+ " 'v1.1': {\n",
+ " 'safetensors': ModelFile('https://huggingface.co/syaimu/7th_Layer/resolve/main/7th_anime_v1/7th_anime_v1.1.safetensors'),\n",
+ " 'ckpt': ModelFile('https://huggingface.co/syaimu/7th_Layer/resolve/main/7th_anime_v1/7th_anime_v1.1.ckpt'),\n",
+ " },\n",
+ " },\n",
+ " 'abyss_7th_layer': {\n",
+ " 'G1': {\n",
+ " 'ckpt': ModelFile('https://huggingface.co/syaimu/7th_Layer/resolve/main/7th_layer/abyss_7th_layerG1.ckpt'),\n",
+ " },\n",
+ " 'ckpt': ModelFile('https://huggingface.co/syaimu/7th_Layer/resolve/main/7th_layer/Abyss_7th_layer.ckpt')\n",
+ " }\n",
+ " }\n",
+ " },\n",
+ "\n",
+ " 'VAEs': {\n",
+ " '$sort': True,\n",
+ "\n",
+ " 'Stable Diffusion': {\n",
+ " 'vae-ft-mse-840000': {\n",
+ " 'pruned': {\n",
+ " 'safetensors': VaeFile(\n",
+ " 'https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.safetensors',\n",
+ " 'stable-diffusion-vae-ft-mse-840000-ema-pruned.safetensors'),\n",
+ " 'ckpt': VaeFile(\n",
+ " 'https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.ckpt',\n",
+ " 'stable-diffusion-vae-ft-mse-840000-ema-pruned.ckpt')\n",
+ " }\n",
+ " },\n",
+ " 'vae-ft-ema-560000': {\n",
+ " 'safetensors': VaeFile(\n",
+ " 'https://huggingface.co/stabilityai/sd-vae-ft-ema-original/resolve/main/vae-ft-ema-560000-ema-pruned.safetensors',\n",
+ " 'stable-diffusion-vae-ft-ema-560000-ema-pruned.safetensors'),\n",
+ " 'ckpt': VaeFile(\n",
+ " 'https://huggingface.co/stabilityai/sd-vae-ft-ema-original/resolve/main/vae-ft-ema-560000-ema-pruned.ckpt',\n",
+ " 'stable-diffusion-vae-ft-ema-560000-ema-pruned.ckpt'),\n",
+ " }\n",
+ " },\n",
+ "\n",
+ " 'Waifu Diffusion': {\n",
+ " 'v1.4': {\n",
+ " 'kl-f8-anime': {\n",
+ " 'e2': {\n",
+ " 'ckpt': VaeFile('https://huggingface.co/hakurei/waifu-diffusion-v1-4/resolve/main/vae/kl-f8-anime2.ckpt'),\n",
+ " },\n",
+ " 'e1': {\n",
+ " 'ckpt': VaeFile('https://huggingface.co/hakurei/waifu-diffusion-v1-4/resolve/main/vae/kl-f8-anime.ckpt'),\n",
+ " }\n",
+ " },\n",
+ " },\n",
+ " },\n",
+ "\n",
+ " 'TrinArt': {\n",
+ " 'autoencoder_fix_kl-f8-trinart_characters': {\n",
+ " 'ckpt': ModelFile('https://huggingface.co/naclbit/trinart_derrida_characters_v2_stable_diffusion/resolve/main/autoencoder_fix_kl-f8-trinart_characters.ckpt')\n",
+ " }\n",
+ " },\n",
+ "\n",
+ " 'NovelAI': {\n",
+ " 'animevae.pt': VaeFile('https://huggingface.co/gozogo123/anime-vae/resolve/main/animevae.pt')\n",
+ " }\n",
+ " },\n",
+ "\n",
+ " 'Textual Inversion (embeddings)': {\n",
+ " '$sort': True,\n",
+ "\n",
+ " 'bad_prompt (negative embedding)': {\n",
+ " 'Version 2': EmbeddingFile('https://huggingface.co/datasets/Nerfgun3/bad_prompt/resolve/main/bad_prompt_version2.pt'),\n",
+ " 'Version 1': EmbeddingFile('https://huggingface.co/datasets/Nerfgun3/bad_prompt/resolve/main/bad_prompt.pt'),\n",
+ " },\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "\n",
+ "def global_disable(disabled: bool):\n",
+ " for dropdown in dropdowns.children:\n",
+ " dropdown.disabled = disabled\n",
+ "\n",
+ " download_button.disabled = disabled\n",
+ "\n",
+ " # 마지막 드롭다운이 하위 드롭다운이라면 버튼 비활성화하기\n",
+ " if not disabled:\n",
+ " dropdown = dropdowns.children[len(dropdowns.children) - 1]\n",
+ " download_button.disabled = isinstance(dropdown, dict)\n",
+ "\n",
+ "\n",
+ "def on_download(_):\n",
+ " dropdown = dropdowns.children[len(dropdowns.children) - 1]\n",
+ " entry = dropdown.entries[dropdown.value]\n",
+ "\n",
+ " global_disable(True)\n",
+ "\n",
+ " # 단일 파일 받기\n",
+ " if isinstance(entry, File):\n",
+ " entry.download()\n",
+ "\n",
+ " # 다중 파일 받기\n",
+ " elif isinstance(entry, list):\n",
+ " for file in entry:\n",
+ " file.download()\n",
+ "\n",
+ " # TODO: 오류 처리\n",
+ " else:\n",
+ " pass\n",
+ "\n",
+ " if DISCONNECT_RUNTIME:\n",
+ " print('파일을 성공적으로 옮겼습니다, 이제 런타임을 해제해도 좋습니다.')\n",
+ "\n",
+ " # 런타임을 바로 종료해버리면 마지막 출력이 잘림\n",
+ " time.sleep(1)\n",
+ " runtime.unassign()\n",
+ "\n",
+ " global_disable(False)\n",
+ "\n",
+ "\n",
+ "def on_dropdown_change(event):\n",
+ " dropdown: widgets.Dropdown = event['owner']\n",
+ " entries: Union[List, Dict] = dropdown.entries[event['new']]\n",
+ "\n",
+ " # 이전 하위 드롭다운 전부 제거하기\n",
+ " dropdowns.children = dropdowns.children[:dropdown.children_index + 1]\n",
+ "\n",
+ " if isinstance(entries, dict):\n",
+ " download_button.disabled = True\n",
+ " create_dropdown(entries)\n",
+ " return\n",
+ "\n",
+ " # 하위 드롭다운 만들기\n",
+ " download_button.disabled = False\n",
+ "\n",
+ "\n",
+ "def create_dropdown(entries: Dict) -> widgets.Dropdown:\n",
+ " if '$sort' in entries and entries['$sort'] == True:\n",
+ " entries = {k: entries[k] for k in sorted(entries)}\n",
+ " del entries['$sort']\n",
+ "\n",
+ " options = list(entries.keys())\n",
+ " value = options[0]\n",
+ "\n",
+ " dropdown = widgets.Dropdown(\n",
+ " options=options,\n",
+ " value=value)\n",
+ "\n",
+ " setattr(dropdown, 'children_index', len(dropdowns.children))\n",
+ " setattr(dropdown, 'entries', entries)\n",
+ "\n",
+ " dropdowns.children = tuple(list(dropdowns.children) + [dropdown])\n",
+ "\n",
+ " dropdown.observe(on_dropdown_change, names='value')\n",
+ "\n",
+ " on_dropdown_change({\n",
+ " 'owner': dropdown,\n",
+ " 'new': value\n",
+ " })\n",
+ "\n",
+ " return dropdown\n",
+ "\n",
+ "\n",
+ "# 첫 엔트리 드롭다운 만들기\n",
+ "create_dropdown(files)\n",
+ "\n",
+ "download_button.on_click(on_download)\n"
+ ]
+ }
+ ]
+}
\ No newline at end of file