forked from extern/easydiffusion
Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
35dc13ffcf | |||
6b55f385c7 | |||
75a9466232 | |||
2ffe7cc843 | |||
d111911c18 | |||
d55935f8c0 | |||
4def55368c | |||
661eff90fc | |||
66d809e9ed | |||
1bd812a2cf | |||
96e4a4ae03 | |||
f526b2df94 | |||
a2a4b32f7b | |||
cb914ea77f |
@ -10,6 +10,6 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8000
|
||||
EXPOSE 9000
|
||||
|
||||
ENTRYPOINT ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0"]
|
||||
ENTRYPOINT ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0", "--port", "9000"]
|
15
OldPortDockerfile
Normal file
15
OldPortDockerfile
Normal file
@ -0,0 +1,15 @@
|
||||
FROM python:3.9
|
||||
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt update
|
||||
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
ENTRYPOINT ["uvicorn", "old_port_main:app", "--host", "0.0.0.0", "--port", "8000"]
|
30
README.md
30
README.md
@ -12,7 +12,11 @@ Two things:
|
||||
|
||||
All the processing will happen on your computer locally, it does not transmit your prompts or process on any remote server.
|
||||
|
||||
<img src="https://github.com/cmdr2/stable-diffusion-ui/raw/main/media/shot-v4.jpg" height="500" alt="Screenshot of tool">
|
||||
<p float="left">
|
||||
<img src="https://github.com/cmdr2/stable-diffusion-ui/raw/main/media/shot-v3a.jpg" height="500" />
|
||||
<img src="https://github.com/cmdr2/stable-diffusion-ui/raw/main/media/shot-v6a.jpg" height="500" />
|
||||
</p>
|
||||
|
||||
|
||||
# System Requirements
|
||||
1. Computer capable of running Stable Diffusion.
|
||||
@ -24,14 +28,14 @@ All the processing will happen on your computer locally, it does not transmit yo
|
||||
# Installation
|
||||
1. Clone this repository: `git clone https://github.com/cmdr2/stable-diffusion-ui.git` or [download the zip file](https://github.com/cmdr2/stable-diffusion-ui/archive/refs/heads/main.zip) and unzip.
|
||||
2. Open your terminal, and in the project directory run: `./server` (warning: this will take some time during the first run, since it'll download Stable Diffusion's [docker image](https://replicate.com/stability-ai/stable-diffusion), nearly 17 GiB)
|
||||
3. Open http://localhost:8000 in your browser. That's it!
|
||||
3. Open http://localhost:9000 in your browser. That's it!
|
||||
|
||||
If you're getting errors, please check the [Troubleshooting](#troubleshooting) section below.
|
||||
If you're getting errors, please check the [Troubleshooting](https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting) page.
|
||||
|
||||
To stop the server, please run `./server stop`
|
||||
|
||||
# Usage
|
||||
Open http://localhost:8000 in your browser (after running `./server` from step 2 previously).
|
||||
Open http://localhost:9000 in your browser (after running `./server` from step 2 previously).
|
||||
|
||||
## With a text description
|
||||
1. Enter a text prompt, like `a photograph of an astronaut riding a horse` in the textbox.
|
||||
@ -57,24 +61,10 @@ You can also set the configuration like `seed`, `width`, `height`, `num_outputs`
|
||||
|
||||
Use the same `seed` number to get the same image for a certain prompt. This is useful for refining a prompt without losing the basic image design. Enable the `random images` checkbox to get random images.
|
||||
|
||||

|
||||

|
||||
|
||||
# Troubleshooting
|
||||
## './docker-compose.yml' is invalid:
|
||||
> ERROR: The Compose file './docker-compose.yml' is invalid because:
|
||||
> services.stability-ai.deploy.resources.reservations value Additional properties are not allowed ('devices' was unexpected)
|
||||
|
||||
Please ensure you have `docker-compose` version 1.29 or higher. Check `docker-compose --version`, and if required [update it to 1.29](https://docs.docker.com/compose/install/). (Thanks [HVRyan](https://github.com/HVRyan))
|
||||
|
||||
## RuntimeError: Found no NVIDIA driver on your system:
|
||||
If you have an NVIDIA GPU and the latest [NVIDIA driver](http://www.nvidia.com/Download/index.aspx), please ensure that you've installed [nvidia-container-toolkit](https://stackoverflow.com/a/58432877). (Thanks [u/exintrovert420](https://www.reddit.com/user/exintrovert420/))
|
||||
|
||||
## Some other process is already running at port 8000 / port 8000 could not be bound
|
||||
You can override the port used. Please change `docker-compose.yml` inside the project directory, and update the line `8000:8000` to `1337:8000` (or where 1337 is whichever port number you want).
|
||||
|
||||
After doing this, please restart your server, by running `./server restart`.
|
||||
|
||||
After this, you can access the server at `http://localhost:1337` (where 1337 is the new port you specified earlier).
|
||||
The [Troubleshooting wiki page](https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting) contains some common errors and their solutions. Please check that, and if it doesn't work, feel free to [file an issue](https://github.com/cmdr2/stable-diffusion-ui/issues).
|
||||
|
||||
# Behind the scenes
|
||||
This project is a quick way to get started with Stable Diffusion. You do not need to have Stable Diffusion already installed, and do not need any API keys. This project will automatically download Stable Diffusion's docker image, the first time it is run.
|
||||
|
@ -5,7 +5,7 @@ services:
|
||||
container_name: sd
|
||||
ports:
|
||||
- '5000:5000'
|
||||
image: 'r8.im/stability-ai/stable-diffusion@sha256:3080f37ef32771c9984d65033cbe71caa96c69680008bae64cf691724a6df04c'
|
||||
image: 'r8.im/stability-ai/stable-diffusion@sha256:be04660a5b93ef2aff61e3668dedb4cbeb14941e62a3fd5998364a32d613e35e'
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
@ -15,7 +15,7 @@ services:
|
||||
stable-diffusion-ui:
|
||||
container_name: sd-ui
|
||||
ports:
|
||||
- '8000:8000'
|
||||
- '9000:9000'
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
@ -24,5 +24,15 @@ services:
|
||||
depends_on:
|
||||
- stability-ai
|
||||
|
||||
stable-diffusion-old-port-redirect:
|
||||
container_name: sd-old-port-redirect
|
||||
ports:
|
||||
- '8000:8000'
|
||||
build:
|
||||
context: .
|
||||
dockerfile: OldPortDockerfile
|
||||
volumes:
|
||||
- .:/app
|
||||
|
||||
networks:
|
||||
default:
|
180
index.html
180
index.html
@ -66,11 +66,30 @@
|
||||
padding-top: 10px;
|
||||
font-size: small;
|
||||
}
|
||||
.imgUseBtn {
|
||||
.imgSeedLabel {
|
||||
position: absolute;
|
||||
transform: translateX(-100%);
|
||||
margin-top: 5pt;
|
||||
margin-left: -5pt;
|
||||
font-size: 10pt;
|
||||
|
||||
background-color: #333;
|
||||
opacity: 0.8;
|
||||
color: #ddd;
|
||||
border-radius: 3pt;
|
||||
padding: 1pt 3pt;
|
||||
}
|
||||
.imgUseBtn {
|
||||
position: absolute;
|
||||
transform: translateX(-100%);
|
||||
margin-top: 30pt;
|
||||
margin-left: -5pt;
|
||||
}
|
||||
.imgSaveBtn {
|
||||
position: absolute;
|
||||
transform: translateX(-100%);
|
||||
margin-top: 55pt;
|
||||
margin-left: -5pt;
|
||||
}
|
||||
.imgItem {
|
||||
display: inline;
|
||||
@ -102,11 +121,11 @@
|
||||
|
||||
<div id="configHeader"><b>Advanced settings:</b> [<a id="configToggleBtn" href="#">show</a>]</div>
|
||||
<div id="config">
|
||||
<label for="seed">Seed:</label> <input id="seed" name="seed" value="30000"> <input id="random_seed" name="random_seed" type="checkbox" checked> <label for="random_seed">Random Image</label> <br/>
|
||||
<label for="num_outputs">Number of outputs:</label> <select id="num_outputs" name="num_outputs" value="1"><option value="1" selected>1</option><option value="4">4</option></select><br/>
|
||||
<label for="seed">Seed:</label> <input id="seed" name="seed" size="10" value="30000"> <input id="random_seed" name="random_seed" type="checkbox" checked> <label for="random_seed">Random Image</label> <br/>
|
||||
<label for="num_outputs_total">Number of outputs:</label> <input id="num_outputs_total" name="num_outputs_total" value="1" size="4"> <label for="num_outputs_parallel">Generate in parallel:</label> <select id="num_outputs_parallel" name="num_outputs_parallel" value="1"><option value="1" selected>1 image at a time</option><option value="4">4 images at a time</option></select><br/>
|
||||
<label for="width">Width:</label> <select id="width" name="width" value="512"><option value="128">128</option><option value="256">256</option><option value="512" selected>512</option><option value="768">768</option><option value="1024">1024</option></select><br/>
|
||||
<label for="height">Height:</label> <select id="height" name="height" value="512"><option value="128">128</option><option value="256">256</option><option value="512" selected>512</option><option value="768">768</option></select><br/>
|
||||
<label for="num_inference_steps">Number of inference steps:</label> <input id="num_inference_steps" name="num_inference_steps" value="50"><br/>
|
||||
<label for="num_inference_steps">Number of inference steps:</label> <input id="num_inference_steps" name="num_inference_steps" size="4" value="50"><br/>
|
||||
<label for="guidance_scale">Guidance Scale:</label> <input id="guidance_scale" name="guidance_scale" value="75" type="range" min="10" max="200"> <span id="guidance_scale_value"></span><br/>
|
||||
<span id="prompt_strength_container"><label for="prompt_strength">Prompt Strength:</label> <input id="prompt_strength" name="prompt_strength" value="8" type="range" min="0" max="10"> <span id="prompt_strength_value"></span><br/></span><br/>
|
||||
<input id="sound_toggle" name="sound_toggle" type="checkbox" checked> <label for="sound_toggle">Play sound on task completion</label><br/>
|
||||
@ -131,7 +150,8 @@ const SOUND_ENABLED_KEY = "soundEnabled"
|
||||
const HEALTH_PING_INTERVAL = 5 // seconds
|
||||
|
||||
let promptField = document.querySelector('#prompt')
|
||||
let numOutputsField = document.querySelector('#num_outputs')
|
||||
let numOutputsTotalField = document.querySelector('#num_outputs_total')
|
||||
let numOutputsParallelField = document.querySelector('#num_outputs_parallel')
|
||||
let numInferenceStepsField = document.querySelector('#num_inference_steps')
|
||||
let guidanceScaleField = document.querySelector('#guidance_scale')
|
||||
let guidanceScaleValueLabel = document.querySelector('#guidance_scale_value')
|
||||
@ -193,6 +213,12 @@ function setStatus(statusType, msg, msgType) {
|
||||
}
|
||||
}
|
||||
|
||||
function logError(msg, res) {
|
||||
outputMsg.innerHTML = '<span style="color: red">Error: ' + msg + '</span>'
|
||||
console.log('request error', res)
|
||||
setStatus('request', 'error', 'error')
|
||||
}
|
||||
|
||||
function playSound() {
|
||||
const audio = new Audio('/media/ding.mp3')
|
||||
audio.volume = 0.2
|
||||
@ -214,43 +240,10 @@ async function healthCheck() {
|
||||
}
|
||||
}
|
||||
|
||||
async function makeImage() {
|
||||
setStatus('request', 'fetching..')
|
||||
|
||||
makeImageBtn.innerHTML = 'Processing..'
|
||||
makeImageBtn.disabled = true
|
||||
|
||||
outputMsg.innerHTML = 'Fetching..'
|
||||
|
||||
function logError(msg, res) {
|
||||
outputMsg.innerHTML = '<span style="color: red">Error: ' + msg + '</span>'
|
||||
console.log('request error', res)
|
||||
setStatus('request', 'error', 'error')
|
||||
}
|
||||
|
||||
let seed = (randomSeedField.checked ? Math.floor(Math.random() * 10000) : seedField.value)
|
||||
|
||||
let reqBody = {
|
||||
prompt: promptField.value,
|
||||
num_outputs: numOutputsField.value,
|
||||
num_inference_steps: numInferenceStepsField.value,
|
||||
guidance_scale: guidanceScaleField.value / 10,
|
||||
width: widthField.value,
|
||||
height: heightField.value,
|
||||
seed: seed,
|
||||
}
|
||||
|
||||
if (initImagePreview.src.indexOf('data:image/png;base64') !== -1) {
|
||||
reqBody['init_image'] = initImagePreview.src
|
||||
reqBody['prompt_strength'] = promptStrengthField.value / 10
|
||||
|
||||
if (maskImagePreview.src.indexOf('data:image/png;base64') !== -1) {
|
||||
reqBody['mask'] = maskImagePreview.src
|
||||
}
|
||||
}
|
||||
|
||||
// makes a single image. don't call this directly, use makeImage() instead
|
||||
async function doMakeImage(reqBody) {
|
||||
let res = ''
|
||||
let time = new Date().getTime()
|
||||
let seed = reqBody['seed']
|
||||
|
||||
try {
|
||||
res = await fetch('/image', {
|
||||
@ -287,24 +280,10 @@ async function makeImage() {
|
||||
setStatus('request', 'error', 'error')
|
||||
}
|
||||
|
||||
makeImageBtn.innerHTML = 'Make Image'
|
||||
makeImageBtn.disabled = false
|
||||
|
||||
if (isSoundEnabled()) {
|
||||
playSound()
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
time = new Date().getTime() - time
|
||||
time /= 1000
|
||||
|
||||
outputMsg.innerHTML = 'Processed in ' + time + ' seconds. Seed: ' + seed
|
||||
|
||||
imagesContainer.innerHTML = ''
|
||||
|
||||
for (let idx in res.output) {
|
||||
let imgBody = ''
|
||||
|
||||
@ -313,7 +292,7 @@ async function makeImage() {
|
||||
} catch (e) {
|
||||
console.log(imgBody)
|
||||
setStatus('request', 'invalid image', 'error')
|
||||
return
|
||||
continue
|
||||
}
|
||||
|
||||
let imgItem = document.createElement('div')
|
||||
@ -324,12 +303,22 @@ async function makeImage() {
|
||||
img.height = parseInt(reqBody.height)
|
||||
img.src = imgBody
|
||||
|
||||
let imgSeedLabel = document.createElement('span')
|
||||
imgSeedLabel.className = 'imgSeedLabel'
|
||||
imgSeedLabel.innerHTML = 'Seed: ' + seed
|
||||
|
||||
let imgUseBtn = document.createElement('button')
|
||||
imgUseBtn.className = 'imgUseBtn'
|
||||
imgUseBtn.innerHTML = 'Use as Input'
|
||||
|
||||
let imgSaveBtn = document.createElement('button')
|
||||
imgSaveBtn.className = 'imgSaveBtn'
|
||||
imgSaveBtn.innerHTML = 'Download'
|
||||
|
||||
imgItem.appendChild(img)
|
||||
imgItem.appendChild(imgSeedLabel)
|
||||
imgItem.appendChild(imgUseBtn)
|
||||
imgItem.appendChild(imgSaveBtn)
|
||||
imagesContainer.appendChild(imgItem)
|
||||
|
||||
imgUseBtn.addEventListener('click', function() {
|
||||
@ -345,7 +334,70 @@ async function makeImage() {
|
||||
seedField.value = seed
|
||||
seedField.disabled = false
|
||||
})
|
||||
|
||||
imgSaveBtn.addEventListener('click', function() {
|
||||
let imgDownload = document.createElement('a')
|
||||
imgDownload.download = generateUUID() + '.png'
|
||||
imgDownload.href = imgBody
|
||||
imgDownload.click()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function makeImage() {
|
||||
setStatus('request', 'fetching..')
|
||||
|
||||
makeImageBtn.innerHTML = 'Processing..'
|
||||
makeImageBtn.disabled = true
|
||||
|
||||
outputMsg.innerHTML = 'Fetching..'
|
||||
|
||||
let seed = (randomSeedField.checked ? Math.floor(Math.random() * 10000) : parseInt(seedField.value))
|
||||
let numOutputsTotal = parseInt(numOutputsTotalField.value)
|
||||
let numOutputsParallel = parseInt(numOutputsParallelField.value)
|
||||
let batchCount = Math.ceil(numOutputsTotal / numOutputsParallel)
|
||||
let batchSize = numOutputsParallel
|
||||
|
||||
let reqBody = {
|
||||
prompt: promptField.value,
|
||||
num_outputs: batchSize,
|
||||
num_inference_steps: numInferenceStepsField.value,
|
||||
guidance_scale: parseInt(guidanceScaleField.value) / 10,
|
||||
width: widthField.value,
|
||||
height: heightField.value,
|
||||
}
|
||||
|
||||
if (initImagePreview.src.indexOf('data:image/png;base64') !== -1) {
|
||||
reqBody['init_image'] = initImagePreview.src
|
||||
reqBody['prompt_strength'] = parseInt(promptStrengthField.value) / 10
|
||||
|
||||
if (maskImagePreview.src.indexOf('data:image/png;base64') !== -1) {
|
||||
reqBody['mask'] = maskImagePreview.src
|
||||
}
|
||||
}
|
||||
|
||||
let time = new Date().getTime()
|
||||
imagesContainer.innerHTML = ''
|
||||
|
||||
for (let i = 0; i < batchCount; i++) {
|
||||
reqBody['seed'] = seed + i
|
||||
|
||||
await doMakeImage(reqBody)
|
||||
|
||||
outputMsg.innerHTML = 'Processed batch ' + (i+1) + '/' + batchCount
|
||||
}
|
||||
|
||||
makeImageBtn.innerHTML = 'Make Image'
|
||||
makeImageBtn.disabled = false
|
||||
|
||||
if (isSoundEnabled()) {
|
||||
playSound()
|
||||
}
|
||||
|
||||
time = new Date().getTime() - time
|
||||
time /= 1000
|
||||
|
||||
outputMsg.innerHTML = 'Processed ' + numOutputsTotal + ' images in ' + time + ' seconds'
|
||||
|
||||
setStatus('request', 'done', 'success')
|
||||
|
||||
@ -354,6 +406,22 @@ async function makeImage() {
|
||||
}
|
||||
}
|
||||
|
||||
function generateUUID() { // Public Domain/MIT
|
||||
var d = new Date().getTime();//Timestamp
|
||||
var d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now()*1000)) || 0;//Time in microseconds since page-load or 0 if unsupported
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
var r = Math.random() * 16;//random number between 0 and 16
|
||||
if(d > 0){//Use timestamp until depleted
|
||||
r = (d + r)%16 | 0;
|
||||
d = Math.floor(d/16);
|
||||
} else {//Use microseconds since page-load if supported
|
||||
r = (d2 + r)%16 | 0;
|
||||
d2 = Math.floor(d2/16);
|
||||
}
|
||||
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
function handleAudioEnabledChange(e) {
|
||||
localStorage.setItem(SOUND_ENABLED_KEY, e.target.checked.toString())
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 18 KiB |
BIN
media/config-v3.jpg
Normal file
BIN
media/config-v3.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
Before Width: | Height: | Size: 51 KiB |
BIN
media/shot-v3a.jpg
Normal file
BIN
media/shot-v3a.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 122 KiB |
Binary file not shown.
Before Width: | Height: | Size: 182 KiB |
BIN
media/shot-v6a.jpg
Normal file
BIN
media/shot-v6a.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
38
old_port_main.py
Normal file
38
old_port_main.py
Normal file
@ -0,0 +1,38 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get('/', response_class=HTMLResponse)
|
||||
def read_root():
|
||||
return '''
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial;
|
||||
font-size: 11pt;
|
||||
}
|
||||
pre {
|
||||
display: inline;
|
||||
background: #aaa;
|
||||
padding: 2px;
|
||||
border: 1px solid #777;
|
||||
border-radius: 3px;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: rgb(32, 33, 36);
|
||||
color: #eee;
|
||||
}
|
||||
pre {
|
||||
background: #444;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<h4>The UI has moved to <a href="http://localhost:9000">http://localhost:9000</a>. The current address that you used (ending with :8000) will be removed in the future, so please use <a href="http://localhost:9000">http://localhost:9000</a> going ahead (and in any bookmarks you've saved).</h4>
|
||||
|
||||
<h4>Also, please use <pre>./server</pre> instead of <pre>docker-compose up &</pre>. To stop, please use <pre>./server stop</pre>. This will help the project better manage the startup process in the future.</h4>
|
||||
|
||||
<h3>Why has the address changed?</h3>
|
||||
<p>The previously used port (8000) is often used by other servers, which results in port conflicts. So the project's port number has been changed, while the project is still young. Otherwise port-conflicts with 8000 will be a common source of new-user issues in the future.</p>
|
||||
<p>Sorry about this, and apologies for the inconvenience :)</p>
|
||||
'''
|
Reference in New Issue
Block a user