Initial release 1.0.0 for sd-webui-bluescape
|
|
@ -0,0 +1,160 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 Thought Stream, LLC dba Bluescape.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
110
README.md
|
|
@ -1 +1,109 @@
|
|||
# sd-webui-bluescape
|
||||
# Bluescape Extension for Stable Diffusion WebUI
|
||||
|
||||
Upload generated images to a Bluescape workspace for review and collaborate with others.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
1. Open "Extensions" tab.
|
||||
2. Open "Install from URL" tab in the tab.
|
||||
3. Enter `https://github.com/Bluescape/sd-webui-bluescape` to "URL for extension's git repository".
|
||||
4. Press "Install" button.
|
||||
5. Wait for 5 seconds, and you will see the message "Installed into stable-diffusion-webui\extensions\sd-webui-bluescape. Use Installed tab to restart".
|
||||
6. Go to "Installed" tab, click "Check for updates", and then click "Apply and restart UI". (The next time you can also use these buttons to update the extension.)
|
||||
7. Completely restart A1111 webui including the backend through your terminal
|
||||
|
||||
_Note: Currently the Bluescape extension only supports the standard A1111 domain / port `(localhost:7860)`._
|
||||
|
||||
## Getting started
|
||||
|
||||
Get started using the Bluescape extension:
|
||||
|
||||
1. **Open Extension**: Open the Bluescape tab on the A1111 webui
|
||||
2. **Register**: If you haven’t done so yet, register for a free account by clicking the button
|
||||
3. **Login**: Log into your account
|
||||
4. **Select Workspace**: Choose the workspace where you’d like to upload generated images
|
||||
5. **Enable Upload Option**: Navigate to either the ‘txt2img’ or ‘img2img’ tab and select the “Upload results to Bluescape” option
|
||||
6. **Generate**: Generate images
|
||||
7. **Review**: Open your workspace to review, curate and collaborate on the generated images
|
||||
|
||||
_Note: The free account has a limit on the number of workspaces and amount of data that can be stored in the workspace. Additional resources are available through paid plans._
|
||||
|
||||

|
||||
|
||||
## Configuration options
|
||||
|
||||
These are the defaults for the extension configuration options:
|
||||
|
||||

|
||||
|
||||
### Include extended generation data in workspace
|
||||
|
||||
By default the extension adds a text field to the workspace with the basic generation data:
|
||||
|
||||

|
||||
|
||||
Enabling extended generation data adds another text field with additional generation information:
|
||||
|
||||

|
||||
|
||||
_Note: The default generation data added to the Bluescape workspace can be copied back to Automatic1111 and re-applied through the prompt._
|
||||
|
||||
### Include source image in workspace (img2img)
|
||||
|
||||
Whether or not to include the source image in the workspace when using img2img:
|
||||
|
||||

|
||||
|
||||
### Include image mask in workspace (img2img)
|
||||
|
||||
Whether or not to include the image mask in the workspace when using img2img with masks.
|
||||
|
||||

|
||||
|
||||
### Scale images to standard size (1000x1000) in workspace
|
||||
|
||||
Whether to scale images to standard Bluescape image size of 1000x1000 in the workspace.
|
||||
|
||||
Note, that this only applies scaling at the workspace level and does not actually change the image data. You can always resize to original size in the workspace as well:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
This is a good option to enable when you have a lot of images in the workspace from different sources and it works well when using images between 500-1000. However, if you work on large images of different aspect ratios it is best to turn this off.
|
||||
|
||||
### Store metadata in image object within workspace
|
||||
|
||||
When this is enabled the generation data and extended generation data are stored as metadata into the Bluescape workspace image elements. This is in anticipation of future Bluescape functionality that makes it easier to copy the generation data back to A1111 and other workflow improvements as well
|
||||
|
||||
You can disable this but it may limit the uploaded images from taking advantage of future A1111 related improvements in Bluescape.
|
||||
|
||||
### Send extension usage analytics
|
||||
|
||||
Whether to send analytics events about user registration, login and upload events. This helps Bluescape assess the amount of use the extension.
|
||||
|
||||
You can disable this if you'd like.
|
||||
|
||||
## Future aspirations
|
||||
|
||||
- Upload img2img masks to the workspace
|
||||
- Upload ControlNet source image and mask to the workspace
|
||||
- Image element context menu item to copy the generation data in A1111 webui compatible format
|
||||
- A1111 running in non-default domain/port
|
||||
- Retrigger image generation with same or modified parameters in A1111 directly from the workspace
|
||||
- Additional layout options
|
||||
- Browse and upload existing generated images from A1111
|
||||
|
||||
## Feedback and more information
|
||||
|
||||
Feel free to send us any feedback you may have either through Github or
|
||||
[Bluescape community forums](https://community.bluescape.com/).
|
||||
|
||||
The extension is open source and licensed under [MIT License](LICENSE).
|
||||
|
||||
If you are interested in developing on the Bluescape platform, please take a look at the [developer documentation](https://api.apps.us.bluescape.com/docs/).
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
#
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2023 Thought Stream, LLC dba Bluescape.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
__version__ = "2.11.2"
|
||||
|
||||
from .extension import BluescapeUploadManager
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
#
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2023 Thought Stream, LLC dba Bluescape.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
from datetime import datetime
|
||||
from .state_manager import StateManager
|
||||
import requests
|
||||
from .config import Config
|
||||
|
||||
class AnalyticsEvent:
|
||||
componentId = str
|
||||
category = str
|
||||
type = str
|
||||
containsPII = bool
|
||||
containsConfidential = bool
|
||||
date = datetime
|
||||
organizationId = str
|
||||
workspaceId = str
|
||||
actorType = str
|
||||
data = object
|
||||
|
||||
class Analytics:
|
||||
|
||||
category = "AI"
|
||||
componentId = "automatic1111-extension"
|
||||
|
||||
def __init__(self, state: StateManager):
|
||||
self.state = state
|
||||
|
||||
# Public
|
||||
|
||||
def send_user_attempting_registration_event(self, registration_attempt_id):
|
||||
e = AnalyticsEvent()
|
||||
e.category = self.category
|
||||
e.componentId = self.componentId
|
||||
e.type = "UserAttemptingRegistration"
|
||||
e.containsPII = False
|
||||
e.containsConfidential = False
|
||||
e.workspaceId = None
|
||||
e.date = datetime.now()
|
||||
e.data = {
|
||||
"registrationAttemptId": registration_attempt_id,
|
||||
"a1111Version": self.state.a1111_version,
|
||||
"extensionVersion": self.state.extension_version
|
||||
}
|
||||
|
||||
self.send_event(e, None)
|
||||
|
||||
def send_user_logged_in_event(self, token, user_id):
|
||||
e = AnalyticsEvent()
|
||||
e.category = self.category
|
||||
e.componentId = self.componentId
|
||||
e.type = "UserLoggedIn"
|
||||
e.containsPII = False
|
||||
e.containsConfidential = False
|
||||
e.workspaceId = None
|
||||
e.date = datetime.now()
|
||||
e.data = {
|
||||
"userId": user_id,
|
||||
"a1111Version": self.state.a1111_version,
|
||||
"extensionVersion": self.state.extension_version
|
||||
}
|
||||
|
||||
self.send_event(e, token)
|
||||
|
||||
def send_uploaded_generated_images_event(self, token, workspace_id, images_number, user_id):
|
||||
e = AnalyticsEvent()
|
||||
e.category = self.category
|
||||
e.componentId = self.componentId
|
||||
e.type = "UserUploadedGeneratedImages"
|
||||
e.containsPII = False
|
||||
e.containsConfidential = False
|
||||
e.workspaceId = workspace_id
|
||||
e.date = datetime.now()
|
||||
e.data = {
|
||||
"imageCount": images_number,
|
||||
"userId": user_id,
|
||||
"a1111Version": self.state.a1111_version,
|
||||
"extensionVersion": self.state.extension_version
|
||||
}
|
||||
|
||||
self.send_event(e, token)
|
||||
|
||||
# Private
|
||||
|
||||
def send_event(self, e: AnalyticsEvent, token):
|
||||
|
||||
if self.state.enable_analytics:
|
||||
body = {
|
||||
'events': [
|
||||
{
|
||||
'category': e.category,
|
||||
'componentId': e.componentId,
|
||||
'type': e.type,
|
||||
'containsPII': e.containsPII,
|
||||
'containsConfidential': e.containsConfidential,
|
||||
'date': datetime.utcnow().isoformat() + 'Z',
|
||||
'data': e.data,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if e.workspaceId is not None:
|
||||
body['events'][0]['workspaceId'] = e.workspaceId
|
||||
|
||||
url = f'{Config.analytics_base_domain}/api/v3/collect'
|
||||
response = requests.post(url, json = body, headers = self.get_headers(token))
|
||||
if response.status_code != 200:
|
||||
print("Analytics response: " + str(response.text))
|
||||
|
||||
def get_headers(self, token):
|
||||
if token is not None:
|
||||
return {
|
||||
#'Authorization': f'Bearer {token}',
|
||||
'Content-type': 'application/json'
|
||||
}
|
||||
|
||||
return {
|
||||
'Content-type': 'application/json'
|
||||
}
|
||||
|
|
@ -0,0 +1,619 @@
|
|||
#
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2023 Thought Stream, LLC dba Bluescape.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
import re
|
||||
from .expired_token_exception import ExpiredTokenException
|
||||
from .config import Config
|
||||
from typing import Tuple
|
||||
import requests
|
||||
import json
|
||||
|
||||
default_timeout = 30
|
||||
|
||||
def bs_find_space(token, workspace_id, bounding_box: Tuple[int, int, int, int]) -> Tuple[int, int, int, int]:
|
||||
|
||||
x, y, width, height = bounding_box
|
||||
|
||||
bs_api_url = f'{Config.api_base_domain}/v3/workspaces/{workspace_id}/findAvailableArea'
|
||||
|
||||
body = {
|
||||
"direction": "Right",
|
||||
"proposedArea": {
|
||||
"x": x,
|
||||
"y": y,
|
||||
"width": width,
|
||||
"height": height
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(bs_api_url, json = body, headers = get_headers(token), timeout = default_timeout)
|
||||
|
||||
if response.status_code == 200:
|
||||
|
||||
result = response.json()
|
||||
x = int(result["x"])
|
||||
y = int(result["y"])
|
||||
width = int(result["width"])
|
||||
height = int(result["height"])
|
||||
|
||||
return (x, y, width, height)
|
||||
|
||||
elif response.status_code == 401:
|
||||
raise ExpiredTokenException
|
||||
|
||||
def bs_create_zygote_at(token, workspace_id, filename, x, y, width, height, traits):
|
||||
|
||||
bs_api_url = f'{Config.api_base_domain}/v3/workspaces/{workspace_id}/elements'
|
||||
|
||||
body = {
|
||||
'type': 'Image',
|
||||
'imageFormat': 'png',
|
||||
'title': filename,
|
||||
'filename': filename,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'transform': {
|
||||
'x': x,
|
||||
'y': y
|
||||
},
|
||||
'traits': {
|
||||
'content': {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for k, v in traits.items():
|
||||
body['traits']['content'][k] = v
|
||||
|
||||
response = requests.post(bs_api_url, json = body, headers = get_headers(token), timeout = default_timeout)
|
||||
|
||||
return response.text
|
||||
|
||||
def bs_upload_asset(zr, buffer):
|
||||
|
||||
body = {
|
||||
'key': zr['data']['content']['fields']['key'],
|
||||
'bucket': zr['data']['content']['fields']['bucket'],
|
||||
'X-Amz-Algorithm': zr['data']['content']['fields']['X-Amz-Algorithm'],
|
||||
'X-Amz-Credential': zr['data']['content']['fields']['X-Amz-Credential'],
|
||||
'X-Amz-Date': zr['data']['content']['fields']['X-Amz-Date'],
|
||||
'Policy': zr['data']['content']['fields']['Policy'],
|
||||
'X-Amz-Signature': zr['data']['content']['fields']['X-Amz-Signature'],
|
||||
}
|
||||
|
||||
files = { 'file': buffer}
|
||||
|
||||
url = zr['data']['content']['url']
|
||||
response = requests.post(url, data = body, files = files, timeout = default_timeout)
|
||||
|
||||
return response.text
|
||||
|
||||
def bs_finish_asset(token, workspace_id, upload_id):
|
||||
|
||||
bs_elementary_api_url = f'{Config.api_base_domain}/v3/workspaces/{workspace_id}/assets/uploads/{upload_id}'
|
||||
|
||||
requests.put(bs_elementary_api_url, headers = get_headers(token), json = {}, timeout = default_timeout)
|
||||
|
||||
def bs_upload_image_at(token, workspace_id, buffer, filename, bounding_box: Tuple[int, int, int, int], traits):
|
||||
|
||||
x, y, width, height = bounding_box
|
||||
|
||||
zygote_response = bs_create_zygote_at(token, workspace_id, filename, x, y, width,height, traits)
|
||||
zygote = json.loads(zygote_response)
|
||||
bs_upload_asset(zygote, buffer)
|
||||
bs_finish_asset(token, workspace_id, zygote['data']['content']['uploadId'])
|
||||
|
||||
def bs_create_canvas_at(token, workspace_id, title, bounding_box: Tuple[int, int, int, int], traits):
|
||||
|
||||
x, y, width, height = bounding_box
|
||||
|
||||
body = {
|
||||
'type': 'Canvas',
|
||||
'name': title,
|
||||
'style': {
|
||||
'width': width,
|
||||
'height': height,
|
||||
'fillColor': {
|
||||
'r': 255,
|
||||
'g': 255,
|
||||
'b': 255,
|
||||
'a': 1
|
||||
},
|
||||
'borderColor': {
|
||||
'r': 255,
|
||||
'g': 255,
|
||||
'b': 255,
|
||||
'a': 1
|
||||
},
|
||||
'showName': True
|
||||
},
|
||||
'transform': {
|
||||
'x': x,
|
||||
'y': y
|
||||
},
|
||||
'traits': {
|
||||
'content': {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for k, v in traits.items():
|
||||
body['traits']['content'][k] = v
|
||||
|
||||
url = f'{Config.api_base_domain}/v3/workspaces/{workspace_id}/elements'
|
||||
|
||||
response = requests.post(url, json = body, headers = get_headers(token), timeout = default_timeout)
|
||||
|
||||
response_info = json.loads(response.text)
|
||||
|
||||
return response_info['data']['id']
|
||||
|
||||
def bs_create_text_with_body(token, workspace_id, body):
|
||||
|
||||
url = f'{Config.api_base_domain}/v3/workspaces/{workspace_id}/elements'
|
||||
|
||||
response = requests.post(url, json = body, headers = get_headers(token), timeout = default_timeout)
|
||||
response_info = json.loads(response.text)
|
||||
|
||||
return response_info['data']['id']
|
||||
|
||||
def bs_create_top_title(token, workspace_id, location: Tuple[int, int, int], text):
|
||||
|
||||
title = " | " + text
|
||||
|
||||
body = {
|
||||
"type": "Text",
|
||||
"blocks": [
|
||||
{
|
||||
"block": {
|
||||
"content": [
|
||||
{
|
||||
"text": "Automatic1111"
|
||||
},
|
||||
{
|
||||
"span": {
|
||||
"color": { #charcoal50
|
||||
"r": 145,
|
||||
"g": 149,
|
||||
"b": 151,
|
||||
"a": 1
|
||||
},
|
||||
"text": title
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"style": {
|
||||
"fontFamily": "Source_Sans_Pro",
|
||||
"fontSize": 63,
|
||||
"width": location[2],
|
||||
"backgroundColor": {
|
||||
"r": 255,
|
||||
"g": 255,
|
||||
"b": 255,
|
||||
"a": 0
|
||||
},
|
||||
"color": { #charcoal100
|
||||
"r": 46,
|
||||
"g": 54,
|
||||
"b": 61,
|
||||
"a": 1
|
||||
},
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
"transform": {
|
||||
"x": location[0],
|
||||
"y": location[1]
|
||||
}
|
||||
}
|
||||
|
||||
return bs_create_text_with_body(token, workspace_id, body)
|
||||
|
||||
def bs_create_extended_data(token, workspace_id, location: Tuple[int, int, int], extended_generation_data):
|
||||
|
||||
content = []
|
||||
for key_value_pair in extended_generation_data:
|
||||
key = key_value_pair[0]
|
||||
value = key_value_pair[1]
|
||||
content.append(
|
||||
{
|
||||
"span": {
|
||||
"fontWeight": "bold",
|
||||
"text": str(key) + ": "
|
||||
}
|
||||
}
|
||||
)
|
||||
content.append(
|
||||
{
|
||||
"span": {
|
||||
"text": str(value) + ", "
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
body = {
|
||||
"type": "Text",
|
||||
"blocks": [
|
||||
{
|
||||
"block": {
|
||||
"content": content
|
||||
}
|
||||
}
|
||||
],
|
||||
"style": {
|
||||
"fontFamily": "Source_Sans_Pro",
|
||||
"fontSize": 32,
|
||||
"width": location[2],
|
||||
"backgroundColor": { #bluescape20
|
||||
"r": 204,
|
||||
"g": 226,
|
||||
"b": 255,
|
||||
"a": 1
|
||||
},
|
||||
"color": { #charcoal90
|
||||
"r": 67,
|
||||
"g": 74,
|
||||
"b": 80,
|
||||
"a": 1
|
||||
},
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
"transform": {
|
||||
"x": location[0],
|
||||
"y": location[1]
|
||||
}
|
||||
}
|
||||
|
||||
return bs_create_text_with_body(token, workspace_id, body)
|
||||
|
||||
def bs_create_generation_data(token, workspace_id, location: Tuple[int, int, int], infotext):
|
||||
|
||||
infos = infotext.split("\n")
|
||||
|
||||
blocks = []
|
||||
|
||||
# Example with prompt + negative prompt:
|
||||
#
|
||||
# Ellen Ripley from Alien movie, astronaut, [perfect symmetric] helmet on, visor closed, looking up, action pose, standing on background of Nostromo space shuttle [intricate] interiors , sci-fi
|
||||
# Negative prompt: disfigured, bad
|
||||
# Steps: 30, Sampler: Euler a, CFG scale: 7, Seed: 889387345, Face restoration: CodeFormer, Size: 512x512, Model hash: ad2a33c361, Model: v2-1_768-ema-pruned, Version: v1.3.1
|
||||
|
||||
# Example without negative prompt:
|
||||
#
|
||||
# Ellen Ripley from Alien movie, astronaut, [perfect symmetric] helmet on, visor closed, looking up, action pose, standing on background of Nostromo space shuttle [intricate] interiors , sci-fi
|
||||
# Steps: 30, Sampler: Euler a, CFG scale: 7, Seed: 889387345, Face restoration: CodeFormer, Size: 512x512, Model hash: ad2a33c361, Model: v2-1_768-ema-pruned, Version: v1.3.1
|
||||
|
||||
# Example without prompt, but with negative prompt:
|
||||
#
|
||||
# Negative prompt: disfigured, bad
|
||||
# Steps: 30, Sampler: Euler a, CFG scale: 7, Seed: 889387345, Face restoration: CodeFormer, Size: 512x512, Model hash: ad2a33c361, Model: v2-1_768-ema-pruned, Version: v1.3.1
|
||||
|
||||
# Example without prompt + without negative prompt:
|
||||
#
|
||||
# Steps: 30, Sampler: Euler a, CFG scale: 7, Seed: 889387345, Face restoration: CodeFormer, Size: 512x512, Model hash: ad2a33c361, Model: v2-1_768-ema-pruned, Version: v1.3.1
|
||||
|
||||
# This is a bit hacky, but we'll check if the row starts with "Negative prompt:" or "Steps:" and then turn
|
||||
# on styling for that row (keys to be bolded, values not)
|
||||
|
||||
for info in infos:
|
||||
content = []
|
||||
if info.startswith("Negative prompt:"):
|
||||
key, value = info.split(':', 1)
|
||||
content.append(
|
||||
{
|
||||
"span": {
|
||||
"fontWeight": "bold",
|
||||
"text": str(key) + ": "
|
||||
}
|
||||
}
|
||||
)
|
||||
content.append(
|
||||
{
|
||||
"span": {
|
||||
"text": str(value)
|
||||
}
|
||||
}
|
||||
)
|
||||
elif info.startswith("Steps:"):
|
||||
try:
|
||||
# Regex pattern to match either key: "value with, commas" or key: value
|
||||
pattern = re.compile(r'([^:]+): ("[^"]*"|[^,]*),?')
|
||||
|
||||
# Find all matches in the string
|
||||
matches = pattern.findall(info)
|
||||
|
||||
# Convert matches to a dictionary
|
||||
key_value_pairs = {key.strip(): value.strip(' "')
|
||||
for key, value in matches}
|
||||
|
||||
for key, value in key_value_pairs.items():
|
||||
content.append(
|
||||
{
|
||||
"span": {
|
||||
"fontWeight": "bold",
|
||||
"text": str(key) + ": "
|
||||
}
|
||||
}
|
||||
)
|
||||
content.append(
|
||||
{
|
||||
"span": {
|
||||
"text": str(value) + ", "
|
||||
}
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
print("Failed to format generation data:")
|
||||
print(e)
|
||||
print("Falling back to unformatted string")
|
||||
content.append(
|
||||
{
|
||||
"span": {
|
||||
"text": str(info)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
else:
|
||||
content.append(
|
||||
{
|
||||
"span": {
|
||||
"text": str(info)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
blocks.append(
|
||||
{
|
||||
"block": {
|
||||
"content": content
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
body = {
|
||||
"type": "Text",
|
||||
"blocks": blocks,
|
||||
"style": {
|
||||
"fontFamily": "Source_Sans_Pro",
|
||||
"fontSize": 32,
|
||||
"width": location[2],
|
||||
"backgroundColor": { #bluescape20
|
||||
"r": 204,
|
||||
"g": 226,
|
||||
"b": 255,
|
||||
"a": 1
|
||||
},
|
||||
"color": { #charcoal90
|
||||
"r": 67,
|
||||
"g": 74,
|
||||
"b": 80,
|
||||
"a": 1
|
||||
},
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
"transform": {
|
||||
"x": location[0],
|
||||
"y": location[1]
|
||||
}
|
||||
}
|
||||
|
||||
return bs_create_text_with_body(token, workspace_id, body)
|
||||
|
||||
def bs_create_seed(token, workspace_id, location: Tuple[int, int, int], seed, subseed):
|
||||
|
||||
body = {
|
||||
"type": "Text",
|
||||
"blocks": [
|
||||
{
|
||||
"block": {
|
||||
"content": [
|
||||
{
|
||||
"span": {
|
||||
"fontWeight": "bold",
|
||||
"text": "Seed: "
|
||||
}
|
||||
},
|
||||
{
|
||||
"span": {
|
||||
"text": str(seed)
|
||||
}
|
||||
},
|
||||
{
|
||||
"span": {
|
||||
"fontStyle": "italic",
|
||||
"text": " (sub: " + str(subseed) + ")"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"style": {
|
||||
"fontFamily": "Source_Sans_Pro",
|
||||
"fontSize": 24,
|
||||
"width": location[2],
|
||||
"backgroundColor": { #bluescape20
|
||||
"r": 204,
|
||||
"g": 226,
|
||||
"b": 255,
|
||||
"a": 1
|
||||
},
|
||||
"color": { #charcoal90
|
||||
"r": 67,
|
||||
"g": 74,
|
||||
"b": 80,
|
||||
"a": 1
|
||||
},
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
"transform": {
|
||||
"x": location[0],
|
||||
"y": location[1]
|
||||
}
|
||||
}
|
||||
|
||||
return bs_create_text_with_body(token, workspace_id, body)
|
||||
|
||||
|
||||
|
||||
def bs_create_generation_label(token, workspace_id, location: Tuple[int, int, int], text):
|
||||
|
||||
body = {
|
||||
"type": "Text",
|
||||
"blocks": [
|
||||
{
|
||||
"block": {
|
||||
"content": [
|
||||
{
|
||||
"span": {
|
||||
"fontWeight": "bold",
|
||||
"text": text
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"style": {
|
||||
"fontFamily": "Source_Sans_Pro",
|
||||
"fontSize": 32,
|
||||
"width": location[2],
|
||||
"backgroundColor": {
|
||||
"r": 255,
|
||||
"g": 255,
|
||||
"b": 255,
|
||||
"a": 0
|
||||
},
|
||||
"color": { #charcoal90
|
||||
"r": 67,
|
||||
"g": 74,
|
||||
"b": 80,
|
||||
"a": 1
|
||||
},
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
"transform": {
|
||||
"x": location[0],
|
||||
"y": location[1]
|
||||
}
|
||||
}
|
||||
|
||||
return bs_create_text_with_body(token, workspace_id, body)
|
||||
|
||||
|
||||
def bs_create_label(token, workspace_id, location: Tuple[int, int, int], text):
|
||||
|
||||
body = {
|
||||
"type": "Text",
|
||||
"blocks": [
|
||||
{
|
||||
"block": {
|
||||
"content": [
|
||||
{
|
||||
"span": {
|
||||
"fontWeight": "bold",
|
||||
"text": text
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"style": {
|
||||
"fontFamily": "Source_Sans_Pro",
|
||||
"fontSize": 24,
|
||||
"width": location[2],
|
||||
"backgroundColor": { #cheeseSoft
|
||||
"r": 254,
|
||||
"g": 240,
|
||||
"b": 159,
|
||||
"a": 1
|
||||
},
|
||||
"color": { #charcoal90
|
||||
"r": 67,
|
||||
"g": 74,
|
||||
"b": 80,
|
||||
"a": 1
|
||||
},
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
"transform": {
|
||||
"x": location[0],
|
||||
"y": location[1]
|
||||
}
|
||||
}
|
||||
|
||||
return bs_create_text_with_body(token, workspace_id, body)
|
||||
|
||||
def bs_get_all_workspaces(token):
|
||||
has_next_page = True
|
||||
cursor = None
|
||||
workspaces = []
|
||||
i = 1
|
||||
|
||||
while has_next_page:
|
||||
(current_page, next) = bs_get_workspaces(token, cursor)
|
||||
workspaces = workspaces + current_page
|
||||
cursor = next
|
||||
# Limit for 3 requests for now
|
||||
print(f"Loading workspaces... {str(i)} of 3")
|
||||
i = i + 1
|
||||
has_next_page = next is not None and i < 4
|
||||
|
||||
return workspaces
|
||||
|
||||
def bs_get_workspaces(token, cursor = None):
|
||||
url = f'{Config.api_base_domain}/v3/users/me/workspaces?pageSize=100&includeCount=true&filterBy=associatedWorkspaces eq false&orderBy=contentUpdatedAt desc'
|
||||
|
||||
if (cursor is not None):
|
||||
url = f'{Config.api_base_domain}/v3/users/me/workspaces?cursor={cursor}'
|
||||
|
||||
response = requests.get(url, headers = get_headers(token), timeout = default_timeout)
|
||||
if response.status_code == 200:
|
||||
response_info = json.loads(response.text)
|
||||
return (response_info['workspaces'], response_info['next'])
|
||||
elif response.status_code == 401:
|
||||
raise ExpiredTokenException
|
||||
|
||||
def bs_get_user_id(token):
|
||||
url = f'{Config.api_base_domain}/v3/users/me'
|
||||
|
||||
response = requests.get(url, headers = get_headers(token))
|
||||
|
||||
if response.status_code == 200:
|
||||
response_info = json.loads(response.text)
|
||||
return response_info["id"]
|
||||
|
||||
print("Error: " + response.text)
|
||||
|
||||
def get_headers(token):
|
||||
return {
|
||||
'Authorization': f'Bearer {token}',
|
||||
'Content-type': 'application/json'
|
||||
}
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
#
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2023 Thought Stream, LLC dba Bluescape.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
from typing import List, Tuple
|
||||
import math
|
||||
|
||||
class BluescapeLayout:
|
||||
|
||||
# Padding used for title positioning within the canvas
|
||||
top_title_padding_left = 100
|
||||
top_title_padding_top = 90
|
||||
|
||||
# Padding used for infotext padding from the bottom of the canvas.
|
||||
# If verbose mode enabled the infotext_top_from_bottom is adjusted
|
||||
infotext_padding_left = 100
|
||||
infotext_top_from_bottom = 250
|
||||
|
||||
# Padding used for infobar padding at the bottom of the canvas.
|
||||
bottom_infobar_padding_left = 100
|
||||
bottom_infobar_top_from_bottom = 200
|
||||
|
||||
# Used to make space for seed info panel below each image.
|
||||
vertical_seed_margin = 50
|
||||
margin = 50 # otherwise
|
||||
|
||||
canvas_padding = (400, 500, 50, 50)
|
||||
|
||||
def __init__(self, num_images: int, image_size: Tuple[int, int], verbose_mode: bool):
|
||||
min_num_columns = 3 if image_size[0] >= 768 else 4
|
||||
num_suggested_columns = math.floor(math.sqrt(num_images))
|
||||
num_columns = num_suggested_columns if num_suggested_columns > min_num_columns else min_num_columns
|
||||
|
||||
if (verbose_mode):
|
||||
self.infotext_top_from_bottom = 500
|
||||
self.canvas_padding = (400, 750, 50, 50)
|
||||
|
||||
self.canvas_padding_top = self.canvas_padding[0]
|
||||
self.canvas_padding_left = self.canvas_padding[2]
|
||||
|
||||
self.image_grid_layout, self.image_grid_bounding_box = self._calculate_image_grid_layout(num_images, num_columns, image_size, self.margin)
|
||||
self.canvas_bounding_box = self._create_canvas_bounding_box_at_origin(self.canvas_padding)
|
||||
|
||||
# Translate the image grid to origin + padding
|
||||
self.image_grid_layout = self._translate_image_grid_layout(self.canvas_padding_left, self.canvas_padding_top)
|
||||
|
||||
self.image_height = image_size[1]
|
||||
|
||||
# Public interface
|
||||
|
||||
def get_image_grid_layout(self) -> Tuple[List[Tuple[int, int]]]:
|
||||
return self.image_grid_layout
|
||||
|
||||
def get_canvas_bounding_box(self) -> Tuple[int, int, int, int]:
|
||||
return self.canvas_bounding_box
|
||||
|
||||
def get_label_grid_layout(self) -> Tuple[List[Tuple[int, int]]]:
|
||||
return self._get_seed_grid_layout()
|
||||
|
||||
def translate(self, target_bounding_box: Tuple[int, int, int, int]):
|
||||
new_x, new_y, _, _ = target_bounding_box
|
||||
self.canvas_bounding_box = (new_x, new_y, self.canvas_bounding_box[2], self.canvas_bounding_box[3])
|
||||
self.image_grid_layout = self._translate_image_grid_layout(self.canvas_padding_left + new_x, self.canvas_padding_top + new_y)
|
||||
|
||||
# Text related public functions
|
||||
|
||||
def get_canvas_title(self, text):
|
||||
return self._truncate_string(text, 100)
|
||||
|
||||
def get_top_title_location(self) -> Tuple[int, int, int]:
|
||||
return (self.canvas_bounding_box[0] + self.top_title_padding_left, self.canvas_bounding_box[1] + self.top_title_padding_top, self.canvas_bounding_box[2] - self.top_title_padding_left * 2)
|
||||
|
||||
def get_top_title(self, text):
|
||||
return self._truncate_string(text, 145)
|
||||
|
||||
def get_bottom_infobar_location(self) -> Tuple[int, int, int]:
|
||||
return (self.canvas_bounding_box[0] + self.top_title_padding_left, self.canvas_bounding_box[1] + self.canvas_bounding_box[3] - self.bottom_infobar_top_from_bottom, self.canvas_bounding_box[2] - self.top_title_padding_left * 2)
|
||||
|
||||
def get_infotext_location(self) -> Tuple[int, int, int]:
|
||||
return (self.canvas_bounding_box[0] + self.top_title_padding_left, self.canvas_bounding_box[1] + self.canvas_bounding_box[3] - self.infotext_top_from_bottom, self.canvas_bounding_box[2] - self.top_title_padding_left * 2)
|
||||
|
||||
def get_infotext_label_location(self) -> Tuple[int, int, int]:
|
||||
return (self.canvas_bounding_box[0] + self.top_title_padding_left, self.canvas_bounding_box[1] + self.canvas_bounding_box[3] - self.infotext_top_from_bottom - 74, self.canvas_bounding_box[2] - self.top_title_padding_left * 2)
|
||||
|
||||
def get_extended_generation_data_label_location(self) -> Tuple[int, int, int]:
|
||||
return (self.canvas_bounding_box[0] + self.top_title_padding_left, self.canvas_bounding_box[1] + self.canvas_bounding_box[3] - self.bottom_infobar_top_from_bottom - 74, self.canvas_bounding_box[2] - self.top_title_padding_left * 2)
|
||||
|
||||
|
||||
# Private functions
|
||||
|
||||
def _calculate_image_grid_layout(self, num_images: int, num_columns: int, image_size: Tuple[int, int], margin: int) -> Tuple[List[Tuple[int, int]], Tuple[int, int, int, int]]:
|
||||
"""
|
||||
Calculates a grid layout for the given number of images, based on the desired number of columns,
|
||||
image size, and margin.
|
||||
|
||||
:param num_images: The number of images.
|
||||
:param num_columns: The desired number of columns in the grid.
|
||||
:param image_size: A tuple representing the size (width, height) of each image.
|
||||
:param margin: The margin in pixels between each image.
|
||||
:return: A list of tuples representing the position (x, y) of each image in the grid layout.
|
||||
"""
|
||||
|
||||
image_width, image_height = image_size
|
||||
|
||||
# Calculate the number of rows based on the desired number of columns
|
||||
num_rows = (num_images + num_columns - 1) // num_columns
|
||||
|
||||
# Calculate the canvas width and height based on the number of columns/rows, image size, and margin
|
||||
canvas_width = num_columns * image_width + (num_columns - 1) * margin
|
||||
canvas_height = num_rows * image_height + (num_rows - 1) * margin + self.vertical_seed_margin * num_rows
|
||||
|
||||
# Calculate the width and height of each cell in the grid
|
||||
cell_width = image_width
|
||||
cell_height = image_height
|
||||
|
||||
# Calculate the x and y position of each image in the grid
|
||||
layout = []
|
||||
for i in range(num_images):
|
||||
col = i % num_columns
|
||||
row = i // num_columns
|
||||
x = col * (cell_width + margin)
|
||||
y = row * (cell_height + margin + self.vertical_seed_margin)
|
||||
layout.append((x, y))
|
||||
|
||||
bounding_box = (0, 0, canvas_width, canvas_height)
|
||||
|
||||
return layout, bounding_box
|
||||
|
||||
def _create_canvas_bounding_box_at_origin(self, canvas_padding: Tuple[int, int, int, int]) -> Tuple[int, int, int, int]:
|
||||
"""
|
||||
Adds padding to each side of the bounding box.
|
||||
|
||||
:param bounding_box: A tuple representing the bounding box coordinates (left, top, width, height).
|
||||
:param padding_top: The padding value for the top side.
|
||||
:param padding_bottom: The padding value for the bottom side.
|
||||
:param padding_left: The padding value for the left side.
|
||||
:param padding_right: The padding value for the right side.
|
||||
:return: A tuple representing the new bounding box coordinates with padding applied (left, top, width, height).
|
||||
"""
|
||||
|
||||
x, y, width, height = self.image_grid_bounding_box
|
||||
padding_top, padding_bottom, padding_left, padding_right = canvas_padding
|
||||
|
||||
x = 0
|
||||
y = 0
|
||||
width += padding_left + padding_right
|
||||
height += padding_top + padding_bottom
|
||||
|
||||
return 0, 0, width, height
|
||||
|
||||
def _translate_image_grid_layout(self, x: int, y: int) -> List[Tuple[int, int]]:
|
||||
"""
|
||||
Translates the existing layout to new coordinates based on the target (x, y).
|
||||
|
||||
:param x: The target x-coordinate for translation.
|
||||
:param y: The target y-coordinate for translation.
|
||||
:param layout: A list of tuples representing the position (x, y) of each image in the layout.
|
||||
:return: A list of tuples representing the new layout with translated coordinates.
|
||||
"""
|
||||
|
||||
# Calculate the translation delta
|
||||
delta_x = x - self.image_grid_layout[0][0]
|
||||
delta_y = y - self.image_grid_layout[0][1]
|
||||
|
||||
# Translate the layout coordinates
|
||||
new_layout = [(lx + delta_x, ly + delta_y) for lx, ly in self.image_grid_layout]
|
||||
|
||||
return new_layout
|
||||
|
||||
def _get_seed_grid_layout(self) -> List[Tuple[int, int]]:
|
||||
|
||||
# TODO: this is silly - the magic 13
|
||||
# Translation delta is the height of the image
|
||||
delta_x = 13
|
||||
delta_y = self.image_height + 13
|
||||
|
||||
# Translate the layout coordinates
|
||||
seed_grid_layout = [(lx + delta_x, ly + delta_y) for lx, ly in self.image_grid_layout]
|
||||
|
||||
return seed_grid_layout
|
||||
|
||||
def _truncate_string(self, text, max_length):
|
||||
if len(text) > max_length:
|
||||
truncated_text = text[:max_length-3] + "..."
|
||||
else:
|
||||
truncated_text = text
|
||||
return truncated_text
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
#
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2023 Thought Stream, LLC dba Bluescape.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
import os
|
||||
import io
|
||||
from .expired_token_exception import ExpiredTokenException
|
||||
from .extension import BluescapeUploadManager
|
||||
from .config import Config
|
||||
import modules.scripts as scripts
|
||||
from modules.processing import Processed, StableDiffusionProcessingImg2Img, StableDiffusionProcessingTxt2Img
|
||||
import gradio as gr
|
||||
from .traits import get_canvas_traits, get_image_traits
|
||||
from .bluescape_layout import BluescapeLayout
|
||||
import uuid
|
||||
|
||||
class Script(scripts.Script):
|
||||
|
||||
manager = BluescapeUploadManager()
|
||||
manager.initialize()
|
||||
|
||||
def show(self, is_img2img):
|
||||
return scripts.AlwaysVisible
|
||||
|
||||
def title(self):
|
||||
self.manager.state.read_versions(self.filename)
|
||||
return "Bluescape upload extension"
|
||||
|
||||
def ui(self, is_img2img):
|
||||
|
||||
with gr.Row(variant="panel"):
|
||||
do_upload = gr.Checkbox(info="Bluescape", label="Upload results to Bluescape")
|
||||
with gr.Row(variant="panel"):
|
||||
# Not pretty
|
||||
if is_img2img:
|
||||
status = gr.HTML(value="""
|
||||
<div style="margin-bottom: var(--spacing-lg); color: var(--block-info-text-color); font-weight: var(--block-info-text-weight); font-size: var(--block-info-text-size); line-height: var(--line-sm);">Status<div>
|
||||
<div style="margin-top: var(--spacing-lg); color: var(--body-text-color); font-weight: var(--checkbox-label-text-weight); font-size: var(--checkbox-label-text-size);" id="bluescape-status-img2img">Idle</div>
|
||||
""")
|
||||
else:
|
||||
status = gr.HTML(value="""
|
||||
<div style="margin-bottom: var(--spacing-lg); color: var(--block-info-text-color); font-weight: var(--block-info-text-weight); font-size: var(--block-info-text-size); line-height: var(--line-sm);">Status<div>
|
||||
<div style="margin-top: var(--spacing-lg); color: var(--body-text-color); font-weight: var(--checkbox-label-text-weight); font-size: var(--checkbox-label-text-size);" id="bluescape-status-txt2img">Idle<div>
|
||||
""")
|
||||
return [do_upload, status]
|
||||
|
||||
def process(self, p, *args):
|
||||
# Uploads the UI state
|
||||
self.manager.set_status("Waiting...", self.is_txt2img)
|
||||
|
||||
# Only for AlwaysVisible scripts
|
||||
def postprocess(self, p, processed: Processed, do_upload, _):
|
||||
|
||||
if do_upload == True:
|
||||
# Lets generate a consistent id for this upload session
|
||||
upload_id = str(uuid.uuid4())
|
||||
print(f"Uploading images to Bluescape - (upload_id: {upload_id})")
|
||||
|
||||
# Uploads the UI state
|
||||
self.manager.set_status("Preparing...", self.is_txt2img)
|
||||
|
||||
# Check for some common settings
|
||||
enable_verbose = self.manager.state.enable_verbose
|
||||
enable_metadata = self.manager.state.enable_metadata
|
||||
img2img_include_init_images = self.manager.state.img2img_include_init_images
|
||||
img2img_include_mask_image = self.manager.state.img2img_include_mask_image
|
||||
scale_to_standard_size = self.manager.state.scale_to_standard_size
|
||||
|
||||
# Detect what we are using to generate
|
||||
generation_type = "unknown"
|
||||
if self.is_txt2img and not self.is_img2img:
|
||||
generation_type = "txt2img"
|
||||
p_img2img = None
|
||||
elif self.is_img2img and not self.is_txt2img:
|
||||
generation_type = "img2img"
|
||||
p_img2img: StableDiffusionProcessingImg2Img = p
|
||||
else:
|
||||
print(f"Unkown image generation type - txt2img: {self.is_txt2img}, img2img: {self.is_img2img} - (upload_id: {upload_id})")
|
||||
|
||||
# Detect number of images we are dealing with
|
||||
index_of_first_image = processed.index_of_first_image
|
||||
|
||||
num_init_images = 0
|
||||
include_mask = False
|
||||
|
||||
if img2img_include_init_images and generation_type == "img2img" and p_img2img:
|
||||
num_init_images = len(p_img2img.init_images)
|
||||
|
||||
if img2img_include_mask_image and p_img2img.image_mask is not None:
|
||||
num_init_images = num_init_images + 1
|
||||
include_mask = True
|
||||
|
||||
num_generated_images = len(processed.images) - index_of_first_image
|
||||
num_images = num_init_images + num_generated_images
|
||||
|
||||
image_size = (1000, 1000) if scale_to_standard_size else (processed.width, processed.height)
|
||||
|
||||
# Lets calculate the layout
|
||||
layout = BluescapeLayout(num_images, image_size, enable_verbose)
|
||||
# How much space we need
|
||||
canvas_bounding_box = layout.get_canvas_bounding_box()
|
||||
|
||||
try:
|
||||
|
||||
# Find available space for us
|
||||
available_canvas_bounding_box = self.manager.find_space(canvas_bounding_box)
|
||||
# Move layout to target that
|
||||
layout.translate(available_canvas_bounding_box)
|
||||
|
||||
# Create canvas
|
||||
canvas_title = layout.get_canvas_title("Automatic1111 | " + processed.prompt)
|
||||
canvas_traits = get_canvas_traits(enable_metadata, processed, generation_type, upload_id, num_images)
|
||||
canvas_id = self.manager.create_canvas_at(canvas_title, available_canvas_bounding_box, canvas_traits)
|
||||
|
||||
# Create title inside the canvas
|
||||
top_title_location = layout.get_top_title_location()
|
||||
top_title = layout.get_top_title(processed.prompt)
|
||||
self.manager.create_top_title(top_title_location, top_title)
|
||||
|
||||
# Create generation data section regardless
|
||||
infotext_location = layout.get_infotext_location()
|
||||
self.manager.create_generation_data(infotext_location, processed.infotexts[0])
|
||||
infotext_label_location = layout.get_infotext_label_location()
|
||||
self.manager.create_generation_label(infotext_label_location, f"Generation data ({generation_type}):")
|
||||
|
||||
# Create extended data section only if verbose_mode enabled
|
||||
if (enable_verbose):
|
||||
extended_generation_data = [
|
||||
( "Image CFG scale", processed.image_cfg_scale ),
|
||||
( "Subseed strength", processed.subseed_strength ),
|
||||
( "Seed resize from w", processed.seed_resize_from_w ),
|
||||
( "Seed resize from h", processed.seed_resize_from_h ),
|
||||
( "DDIM Discretize", processed.ddim_discretize ),
|
||||
( "ETA", processed.eta ),
|
||||
( "Clip skip", processed.clip_skip ),
|
||||
( "Sigma churn", processed.s_churn ),
|
||||
( "Sigma noise", processed.s_noise ),
|
||||
( "Sigma tmin", processed.s_tmin ),
|
||||
( "Sigma tmax", processed.s_tmax ),
|
||||
( "Sampler noise scheduler override", processed.sampler_noise_scheduler_override ),
|
||||
( "Is using inpainting conditioning", processed.is_using_inpainting_conditioning ),
|
||||
( "Extra generation params", processed.extra_generation_params ),
|
||||
( "Bluescape upload id", upload_id ),
|
||||
]
|
||||
bottom_small_infobar_location = layout.get_bottom_infobar_location()
|
||||
self.manager.create_extended_data(bottom_small_infobar_location, extended_generation_data)
|
||||
infotext_label_location = layout.get_extended_generation_data_label_location()
|
||||
self.manager.create_generation_label(infotext_label_location, f"Extended generation data:")
|
||||
|
||||
# Get image coordinates
|
||||
image_layout = layout.get_image_grid_layout()
|
||||
# Get seed / label coordinates
|
||||
label_layout = layout.get_label_grid_layout()
|
||||
|
||||
# We'll use the shared 'i' between the first iterator (init images)
|
||||
# and the second one (generated images). This is used to address image_layout
|
||||
# and seed_layout, which account for both sets of images.
|
||||
i = 0
|
||||
|
||||
# First check for init images (source images)
|
||||
if img2img_include_init_images and generation_type == "img2img" and p_img2img:
|
||||
for index, image in enumerate(p_img2img.init_images):
|
||||
seed = "source_image"
|
||||
subseed = "unknown"
|
||||
infotext = "source_image"
|
||||
|
||||
self.manager.set_status(f"Uploading image: {i + 1} / {num_images}", self.is_txt2img)
|
||||
|
||||
traits = get_image_traits(enable_metadata, processed, seed, subseed, infotext, generation_type, upload_id)
|
||||
|
||||
x, y = image_layout[i]
|
||||
width, height = image_size
|
||||
|
||||
# Filename based on the index
|
||||
filename = f"source-image_{index}.png"
|
||||
png_data = io.BytesIO()
|
||||
image.save(png_data, format="PNG")
|
||||
self.manager.upload_image_at(png_data.getvalue(), filename, (x, y, width, height), traits)
|
||||
|
||||
label_x, label_y = label_layout[i]
|
||||
label_width = image_size[0]
|
||||
label_height = 50
|
||||
self.manager.create_label((label_x, label_y, label_width, label_height), "Source image")
|
||||
|
||||
i += 1
|
||||
|
||||
print(f"Init image has been uploaded to Bluescape - (upload_id: {upload_id})")
|
||||
|
||||
if include_mask:
|
||||
self.manager.set_status(f"Uploading image: {i + 1} / {num_images}", self.is_txt2img)
|
||||
x, y = image_layout[i]
|
||||
width, height = image_size
|
||||
|
||||
# Filename based on the index
|
||||
filename = f"image_mask.png"
|
||||
png_data = io.BytesIO()
|
||||
p_img2img.image_mask.save(png_data, format="PNG")
|
||||
self.manager.upload_image_at(png_data.getvalue(), filename, (x, y, width, height), traits)
|
||||
|
||||
label_x, label_y = label_layout[i]
|
||||
label_width = image_size[0]
|
||||
label_height = 50
|
||||
self.manager.create_label((label_x, label_y, label_width, label_height), "Image mask")
|
||||
|
||||
i += 1
|
||||
|
||||
print(f"Mask image has been uploaded to Bluescape - (upload_id: {upload_id})")
|
||||
|
||||
# i continues where it was left of above
|
||||
# Now we'll deal with the generated images
|
||||
for index, image in enumerate(processed.images):
|
||||
if index >= index_of_first_image:
|
||||
adjusted_index = index - index_of_first_image
|
||||
|
||||
self.manager.set_status(f"Uploading image: {i + 1} / {num_images}", self.is_txt2img)
|
||||
|
||||
seed = processed.all_seeds[adjusted_index]
|
||||
subseed = processed.all_subseeds[adjusted_index]
|
||||
infotext = processed.infotexts[adjusted_index]
|
||||
|
||||
traits = get_image_traits(enable_metadata, processed, seed, subseed, infotext, generation_type, upload_id)
|
||||
|
||||
x, y = image_layout[i]
|
||||
width, height = image_size
|
||||
|
||||
# Filename based on seed and subseed
|
||||
filename = f"{seed}-{subseed}.png"
|
||||
png_data = io.BytesIO()
|
||||
image.save(png_data, format="PNG")
|
||||
self.manager.upload_image_at(png_data.getvalue(), filename, (x, y, width, height), traits)
|
||||
|
||||
label_x, label_y = label_layout[i]
|
||||
label_width = image_size[0]
|
||||
label_height = 50
|
||||
self.manager.create_seed_label((label_x, label_y, label_width, label_height), seed, subseed)
|
||||
|
||||
i += 1
|
||||
|
||||
print(f"Image {index} has been uploaded to Bluescape - (upload_id: {upload_id})")
|
||||
else:
|
||||
print(f"Ignoring generated image with index {index}, as it is smaller than index of first generated image: {index_of_first_image} - (upload_id: {upload_id})")
|
||||
|
||||
# Provide a link to the canvas back to the UI
|
||||
state = self.manager.state
|
||||
link_to_canvas = f"{Config.client_base_domain}/applink/{state.selected_workspace_id}?objectId={canvas_id}"
|
||||
self.manager.set_status(f"Upload complete - <a href='{link_to_canvas}' target='_blank'>Click here to open workspace</a>", self.is_txt2img)
|
||||
|
||||
# Analytics
|
||||
self.manager.analytics.send_uploaded_generated_images_event(state.user_token, state.selected_workspace_id, num_images, state.user_id)
|
||||
|
||||
print(f"Upload complete - (upload_id: {upload_id})")
|
||||
except ExpiredTokenException:
|
||||
self.manager.state.show_token_expired_status_message = True
|
||||
|
||||
return True
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
#
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2023 Thought Stream, LLC dba Bluescape.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
import os
|
||||
|
||||
class Config:
|
||||
|
||||
api_base_domain = os.getenv('BS_API_BASE_DOMAIN', 'https://api.apps.us.bluescape.com')
|
||||
client_base_domain = os.getenv('BS_CLIENT_BASE_DOMAIN', 'https://client.apps.us.bluescape.com')
|
||||
analytics_base_domain = os.getenv('BS_ANALYTICS_BASE_DOMAIN', 'https://analytics-api.apps.us.bluescape.com')
|
||||
isam_base_domain = os.getenv('BS_ISAM_BASE_DOMAIN', 'https://isam.apps.us.bluescape.com')
|
||||
client_id = os.getenv('BS_CLIENT_ID', 'cbc5407f-1860-4a47-a61f-ec135715aea0')
|
||||
auth_redirect_url = os.getenv('BS_AUTH_REDIRECT', 'http://localhost:7860/bluescape/oauth_callback')
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
#
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2023 Thought Stream, LLC dba Bluescape.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
class ExpiredTokenException(Exception):
|
||||
"Raised when Bluescape token has expired"
|
||||
pass
|
||||
|
|
@ -0,0 +1,327 @@
|
|||
#
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2023 Thought Stream, LLC dba Bluescape.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
from __future__ import annotations
|
||||
import json
|
||||
from typing import Tuple
|
||||
from .expired_token_exception import ExpiredTokenException
|
||||
from .templates import bluescape_auth_function, bluescape_open_workspace_function, login_endpoint_page, refresh_ui_page, registration_endpoint_page, expired_token_message_block
|
||||
from .config import Config
|
||||
from .analytics import Analytics
|
||||
from .bluescape_api import bs_create_extended_data, bs_create_canvas_at, bs_create_generation_label, bs_create_generation_data, bs_create_label, bs_create_seed, bs_create_top_title, bs_find_space, bs_upload_image_at, bs_get_user_id
|
||||
from .state_manager import StateManager
|
||||
from .misc import extract_workspace_id
|
||||
import gradio as gr
|
||||
import modules.scripts as scripts
|
||||
import uuid
|
||||
import pkce
|
||||
from modules import script_callbacks
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import HTMLResponse
|
||||
import requests
|
||||
|
||||
class BluescapeUploadManager:
|
||||
|
||||
state = StateManager()
|
||||
analytics = Analytics(state)
|
||||
|
||||
def initialize(self):
|
||||
self.state.load()
|
||||
script_callbacks.on_ui_tabs(self.on_ui_tabs)
|
||||
script_callbacks.on_app_started(self.on_app_start)
|
||||
|
||||
def bluescape_login_endpoint(self):
|
||||
self.code_verifier, challenge = pkce.generate_pkce_pair()
|
||||
return login_endpoint_page(challenge)
|
||||
|
||||
def bluescape_oauth_callback_endpoint(self, code):
|
||||
print("Received callback on /bluescape/oauth_callback")
|
||||
url = f"{Config.isam_base_domain}/api/v3/oauth2/token"
|
||||
|
||||
response = requests.post(url, headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
|
||||
data = {
|
||||
'code': code,
|
||||
'grant_type': 'authorization_code',
|
||||
'client_id': Config.client_id,
|
||||
'redirect_uri': Config.auth_redirect_url,
|
||||
'code_verifier': self.code_verifier
|
||||
})
|
||||
|
||||
if response.status_code == 200:
|
||||
response_info = json.loads(response.text)
|
||||
token = response_info['access_token']
|
||||
user_id = bs_get_user_id(token)
|
||||
# refresh_workspaces saves state afterwards
|
||||
self.state.user_token = token
|
||||
self.state.user_id = user_id
|
||||
self.state.save()
|
||||
self.state.show_token_expired_status_message = False
|
||||
self.analytics.send_user_logged_in_event(token, user_id)
|
||||
self.state.refresh_workspaces(token)
|
||||
return refresh_ui_page()
|
||||
else:
|
||||
print("OAuth error:")
|
||||
print(response.text)
|
||||
|
||||
def bluescape_logout_endpoint(self):
|
||||
print("User data is flushed")
|
||||
self.state.flush_user_data()
|
||||
return refresh_ui_page()
|
||||
|
||||
def bluescape_status_endpoint(self):
|
||||
if self.state.show_token_expired_status_message:
|
||||
return expired_token_message_block + "\n" + expired_token_message_block
|
||||
else:
|
||||
return self.state.txt2img_status + "\n" + self.state.img2img_status
|
||||
|
||||
def bluescape_register_endpoint(self):
|
||||
registration_attempt_id = str(uuid.uuid4())
|
||||
self.analytics.send_user_attempting_registration_event(registration_attempt_id)
|
||||
registration_url = f"{Config.client_base_domain}/signup?utm_campaign=automatic_offer&utm_source=campaign_signup&utm_term={registration_attempt_id}"
|
||||
|
||||
return registration_endpoint_page(registration_url)
|
||||
|
||||
def set_status(self, status, is_txt2img):
|
||||
if is_txt2img:
|
||||
self.state.txt2img_status = status
|
||||
else:
|
||||
self.state.img2img_status = status
|
||||
|
||||
def on_app_start(self, _, app: FastAPI):
|
||||
app.add_api_route("/bluescape/login", self.bluescape_login_endpoint, methods=["GET"],
|
||||
response_class=HTMLResponse)
|
||||
app.add_api_route("/bluescape/oauth_callback", self.bluescape_oauth_callback_endpoint, methods=["GET"],
|
||||
response_class=HTMLResponse)
|
||||
app.add_api_route("/bluescape/registration", self.bluescape_register_endpoint, methods=["GET"], response_class=HTMLResponse)
|
||||
app.add_api_route("/bluescape/logout", self.bluescape_logout_endpoint, methods=["GET"], response_class=HTMLResponse)
|
||||
app.add_api_route("/bluescape/status", self.bluescape_status_endpoint, methods=["GET"], response_class=HTMLResponse)
|
||||
print("Bluescape endpoints have been mounted")
|
||||
|
||||
def upload_image_at(self, buffer, filename, bounding_box: Tuple[int, int, int, int], traits):
|
||||
bs_upload_image_at(self.state.user_token, self.state.selected_workspace_id, buffer, filename, bounding_box, traits)
|
||||
|
||||
def create_canvas_at(self, title, bounding_box: Tuple[int, int, int, int], traits):
|
||||
return bs_create_canvas_at(self.state.user_token, self.state.selected_workspace_id, title, bounding_box, traits)
|
||||
|
||||
def find_space(self, bounding_box: Tuple[int, int, int, int]) -> Tuple[int, int, int, int]:
|
||||
return bs_find_space(self.state.user_token, self.state.selected_workspace_id, bounding_box)
|
||||
|
||||
def create_top_title(self, location: Tuple[int, int, int], title):
|
||||
return bs_create_top_title(self.state.user_token, self.state.selected_workspace_id, location, title)
|
||||
|
||||
def create_extended_data(self, location: Tuple[int, int, int], extended_generation_data):
|
||||
return bs_create_extended_data(self.state.user_token, self.state.selected_workspace_id, location, extended_generation_data)
|
||||
|
||||
def create_generation_data(self, location: Tuple[int, int, int], infotext):
|
||||
return bs_create_generation_data(self.state.user_token, self.state.selected_workspace_id, location, infotext)
|
||||
|
||||
def create_seed_label(self, location: Tuple[int, int, int], seed, subseed):
|
||||
return bs_create_seed(self.state.user_token, self.state.selected_workspace_id, location, seed, subseed)
|
||||
|
||||
def create_label(self, location: Tuple[int, int, int], text):
|
||||
return bs_create_label(self.state.user_token, self.state.selected_workspace_id, location, text)
|
||||
|
||||
def create_generation_label(self, location: Tuple[int, int, int], text):
|
||||
return bs_create_generation_label(self.state.user_token, self.state.selected_workspace_id, location, text)
|
||||
|
||||
def get_enable_verbose(self):
|
||||
return self.state.enable_verbose
|
||||
|
||||
def get_img2img_include_init_images(self):
|
||||
return self.state.img2img_include_init_images
|
||||
|
||||
def get_img2img_include_mask_image(self):
|
||||
return self.state.img2img_include_mask_image
|
||||
|
||||
def get_scale_to_standard_size(self):
|
||||
return self.state.scale_to_standard_size
|
||||
|
||||
def get_enable_metadata(self):
|
||||
return self.state.enable_metadata
|
||||
|
||||
def get_enable_analytics(self):
|
||||
return self.state.enable_analytics
|
||||
|
||||
def is_empty_user_token(self):
|
||||
return self.state.user_token == ""
|
||||
|
||||
def get_user_token(self):
|
||||
return self.state.user_token
|
||||
|
||||
def get_workspace_dd(self):
|
||||
return self.state.workspace_dd
|
||||
|
||||
def get_selected_workspace_item(self):
|
||||
return self.state.get_selected_workspace_item()
|
||||
|
||||
def on_ui_tabs(self):
|
||||
|
||||
with gr.Blocks() as bluescape_tab:
|
||||
|
||||
token_source = gr.Textbox(self.get_user_token, visible=False)
|
||||
|
||||
# Outputs: workspaces_dropdown, workspace_to_open_textbox, register_button, refresh_workspaces_button, open_workspace_button, login_button, logout_button
|
||||
def refresh_workspaces_and_ui(input):
|
||||
try:
|
||||
self.state.refresh_workspaces(input)
|
||||
ws_value = self.state.workspace_ids.get(self.state.selected_workspace_id, self.state.workspace_dd[0])
|
||||
return [
|
||||
gr.Dropdown.update(choices=self.state.workspace_dd, visible=True, value=ws_value),
|
||||
gr.Textbox.update(value=self.state.selected_workspace_id),
|
||||
gr.Button.update(visible=False),
|
||||
gr.Button.update(visible=True),
|
||||
gr.Button.update(visible=True),
|
||||
gr.Button.update(visible=False),
|
||||
gr.Button.update(visible=True)
|
||||
]
|
||||
except ExpiredTokenException:
|
||||
print("Bluescape token has expired")
|
||||
self.state.show_token_expired_status_message = True
|
||||
self.state.user_token = ""
|
||||
self.state.save()
|
||||
|
||||
return [
|
||||
gr.Dropdown.update(choices=[], visible=False, value=''),
|
||||
gr.Textbox.update(value=self.state.selected_workspace_id),
|
||||
gr.Button.update(visible=True),
|
||||
gr.Button.update(visible=False),
|
||||
gr.Button.update(visible=False),
|
||||
gr.Button.update(visible=True),
|
||||
gr.Button.update(visible=False),
|
||||
gr.Checkbox.update(visible=True)
|
||||
]
|
||||
|
||||
with gr.Row():
|
||||
with gr.Column():
|
||||
with gr.Row(variant="panel"):
|
||||
gr.Markdown(
|
||||
"""
|
||||
# Instructions
|
||||
|
||||
Get started using the Bluescape extension:
|
||||
|
||||
1. **Register**: If you haven't done so yet, register for a free account
|
||||
2. **Login**: Log into your account
|
||||
3. **Select Workspace**: Choose the workspace where you'd like to upload generated images
|
||||
4. **Enable Upload Option**: Navigate to either the 'txt2img' or 'img2img' tab and select the "Upload results to Bluescape" option
|
||||
5. **Review**: Open your workspace to review, curate and collaborate on the generated images
|
||||
|
||||
|
||||
_Tip: Generation data added to the Bluescape workspace can be copied back to Automatic1111 and re-applied through the prompt._
|
||||
"""
|
||||
)
|
||||
with gr.Row():
|
||||
with gr.Column():
|
||||
login_button = gr.Button(value="Login", elem_id="bluescape-login-button", visible=self.is_empty_user_token())
|
||||
logout_button = gr.Button(value="Logout", elem_id="bluescape-logout-button", visible=(not self.is_empty_user_token()))
|
||||
with gr.Column():
|
||||
register_button = gr.Button(value="Register for a free account", visible=self.is_empty_user_token())
|
||||
with gr.Row():
|
||||
workspaces_dropdown = gr.Dropdown(choices=self.get_workspace_dd(), label="Select target workspace:", value=self.get_selected_workspace_item, elem_id="bluescape-workspaces-dropdown", visible=(not self.is_empty_user_token()))
|
||||
with gr.Row():
|
||||
with gr.Column():
|
||||
workspace_to_open_textbox = gr.Textbox(label="Workspace to open", value=self.get_selected_workspace_item, elem_id="bluescape-open-workspace-id", visible=False)
|
||||
open_workspace_button = gr.Button("Open target workspace", visible=(not self.is_empty_user_token()))
|
||||
with gr.Column():
|
||||
refresh_workspaces_button = gr.Button("Refresh workspaces", visible=(not self.is_empty_user_token()))
|
||||
with gr.Row():
|
||||
gr.Markdown(
|
||||
"""
|
||||
# Configuration
|
||||
""")
|
||||
enable_verbose_checkbox = gr.Checkbox(label="Include extended generation data in workspace", value=self.get_enable_verbose, interactive=True)
|
||||
img2img_include_init_images_checkbox = gr.Checkbox(label="Include source image in workspace (img2img)", value=self.get_img2img_include_init_images, interactive=True)
|
||||
img2img_include_mask_image_checkbox = gr.Checkbox(label="Include image mask in workspace (img2img)", value=self.get_img2img_include_mask_image, interactive=True)
|
||||
scale_to_standard_size_checkbox = gr.Checkbox(label="Scale images to standard size (1000x1000) in workspace", value=self.get_scale_to_standard_size, interactive=True)
|
||||
enable_metadata_checkbox = gr.Checkbox(label="Store metadata in image object within workspace", value=self.get_enable_metadata, interactive=True)
|
||||
enable_analytics_checkbox = gr.Checkbox(label="Send extension usage analytics", value=self.get_enable_analytics, interactive=True)
|
||||
|
||||
with gr.Row(variant="panel"):
|
||||
gr.Markdown(
|
||||
"""
|
||||
# Feedback
|
||||
|
||||
Feel free to send us any feedback you may have. Whether it's praise or constructive criticism.
|
||||
|
||||
To share your thoughts, click [here](https://community.bluescape.com/)
|
||||
|
||||
For documentation on configuration options visit [Github](https://github.com/Bluescape/sd-webui-bluescape)
|
||||
"""
|
||||
)
|
||||
|
||||
token_source.change(refresh_workspaces_and_ui, inputs=[token_source], outputs=[workspaces_dropdown, workspace_to_open_textbox, register_button, refresh_workspaces_button, open_workspace_button, login_button, logout_button])
|
||||
|
||||
|
||||
with gr.Column():
|
||||
# Used to force a second column for better page layout
|
||||
gr.Textbox(label="dummy", interactive=False, visible=False, value="dummy")
|
||||
|
||||
def selected_workspace_change(input):
|
||||
if input is not None and input != "":
|
||||
self.state.selected_workspace_id = extract_workspace_id(input)
|
||||
self.state.save()
|
||||
|
||||
return gr.Textbox.update(value=self.state.selected_workspace_id)
|
||||
|
||||
def enable_analytics_change(input):
|
||||
self.state.enable_analytics = input
|
||||
self.state.save()
|
||||
|
||||
def enable_verbose_change(input):
|
||||
self.state.enable_verbose = input
|
||||
self.state.save()
|
||||
|
||||
def enable_metadata_change(input):
|
||||
self.state.enable_metadata = input
|
||||
self.state.save()
|
||||
|
||||
def img2img_include_init_images_change(input):
|
||||
self.state.img2img_include_init_images = input
|
||||
self.state.save()
|
||||
|
||||
def img2img_include_mask_image_change(input):
|
||||
self.state.img2img_include_mask_image = input
|
||||
self.state.save()
|
||||
|
||||
def scale_to_standard_size_change(input):
|
||||
self.state.scale_to_standard_size = input
|
||||
self.state.save()
|
||||
|
||||
# Event handlers assignment
|
||||
login_button.click(None, _js=bluescape_auth_function)
|
||||
logout_button.click(None, _js="bluescape_logout")
|
||||
open_workspace_button.click(None, _js=bluescape_open_workspace_function)
|
||||
register_button.click(None, _js="bluescape_registration")
|
||||
refresh_workspaces_button.click(refresh_workspaces_and_ui, inputs=[token_source], outputs=[workspaces_dropdown, workspace_to_open_textbox, register_button, refresh_workspaces_button, open_workspace_button, login_button, logout_button])
|
||||
workspaces_dropdown.change(selected_workspace_change, inputs=[workspaces_dropdown], outputs=[workspace_to_open_textbox])
|
||||
enable_verbose_checkbox.change(enable_verbose_change, inputs=[enable_verbose_checkbox])
|
||||
img2img_include_init_images_checkbox.change(img2img_include_init_images_change, inputs=[img2img_include_init_images_checkbox])
|
||||
img2img_include_mask_image_checkbox.change(img2img_include_mask_image_change, inputs=[img2img_include_mask_image_checkbox])
|
||||
scale_to_standard_size_checkbox.change(scale_to_standard_size_change, inputs=[scale_to_standard_size_checkbox])
|
||||
enable_metadata_checkbox.change(enable_metadata_change, inputs=[enable_metadata_checkbox])
|
||||
enable_analytics_checkbox.change(enable_analytics_change, inputs=[enable_analytics_checkbox])
|
||||
|
||||
return ((bluescape_tab, "Bluescape", "bluescape-tab"),)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
#
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2023 Thought Stream, LLC dba Bluescape.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
def extract_workspace_id(input):
|
||||
parts = input.split()
|
||||
workspace_part = parts[-1]
|
||||
workspace_id = workspace_part[1:-1]
|
||||
if len(workspace_id) != 20:
|
||||
raise
|
||||
|
||||
return workspace_id
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
#
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2023 Thought Stream, LLC dba Bluescape.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
import json
|
||||
import os
|
||||
from .bluescape_api import bs_get_all_workspaces
|
||||
from appdirs import AppDirs
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
class StateManager:
|
||||
|
||||
user_token = ""
|
||||
user_id = ""
|
||||
|
||||
# Array for dropdown values - labels
|
||||
workspace_dd = {}
|
||||
# Dictionary to lookup name by workspace id
|
||||
workspace_ids = {}
|
||||
|
||||
selected_workspace_id = ""
|
||||
enable_verbose = False
|
||||
img2img_include_init_images = True
|
||||
scale_to_standard_size = True
|
||||
enable_metadata = True
|
||||
enable_analytics = True
|
||||
|
||||
show_token_expired_status_message = False
|
||||
|
||||
img2img_include_mask_image = False
|
||||
|
||||
txt2img_status = ""
|
||||
img2img_status = ""
|
||||
|
||||
a1111_version = "unknown"
|
||||
extension_version = "unknown"
|
||||
|
||||
def __init__(self):
|
||||
dirs = AppDirs("a1111-sd-extension", "Bluescape")
|
||||
os.makedirs(dirs.user_data_dir, exist_ok=True)
|
||||
self.state_file = os.path.join(dirs.user_data_dir, "bs_state.json")
|
||||
print("State file for your system: " + self.state_file)
|
||||
|
||||
def read_versions(self, extension_file_path):
|
||||
if self.a1111_version != "unknown":
|
||||
return
|
||||
|
||||
try:
|
||||
self.a1111_version = subprocess.check_output(["git", "rev-parse", "HEAD"]).strip().decode()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
extension_root_dir = Path(extension_file_path).parent.parent
|
||||
version_file = str(extension_root_dir) + '/version.txt'
|
||||
with open(version_file) as f:
|
||||
self.extension_version = f.read()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def flush_user_data(self):
|
||||
self.user_token = ""
|
||||
self.user_id = ""
|
||||
self.workspace_dd = {}
|
||||
self.workspace_ids = {}
|
||||
self.selected_workspace_id = ""
|
||||
self.save()
|
||||
|
||||
def refresh_workspaces(self, token):
|
||||
workspaces = bs_get_all_workspaces(token)
|
||||
print(f"Total {len(workspaces)} workspaces retrieved")
|
||||
self.workspace_ids = {}
|
||||
self.workspace_dd = []
|
||||
|
||||
for w in workspaces:
|
||||
dd_name = f"{w['name']} ({w['id']})"
|
||||
self.workspace_ids[w['id']] = dd_name
|
||||
self.workspace_dd.append(dd_name)
|
||||
|
||||
# If we don't have an existing selected id, lets select the first one
|
||||
if self.selected_workspace_id == "" or self.selected_workspace_id not in self.workspace_ids.keys():
|
||||
self.selected_workspace_id = next(iter(self.workspace_ids))
|
||||
|
||||
self.save()
|
||||
|
||||
def get_selected_workspace_item(self):
|
||||
selected_workspace = None
|
||||
if self.selected_workspace_id is not None and self.selected_workspace_id != "" and self.selected_workspace_id in self.workspace_ids:
|
||||
selected_workspace = self.workspace_ids[self.selected_workspace_id]
|
||||
|
||||
return selected_workspace
|
||||
|
||||
def save(self):
|
||||
with open(self.state_file, "w+") as out_file:
|
||||
json.dump({
|
||||
"user_id": self.user_id,
|
||||
"selected_workspace_id": self.selected_workspace_id,
|
||||
"enable_verbose": self.enable_verbose,
|
||||
"img2img_include_init_images": self.img2img_include_init_images,
|
||||
"img2img_include_mask_image": self.img2img_include_mask_image,
|
||||
"scale_to_standard_size": self.scale_to_standard_size,
|
||||
"enable_metadata": self.enable_metadata,
|
||||
"enable_analytics": self.enable_analytics,
|
||||
"workspace_dd": self.workspace_dd,
|
||||
"workspace_ids": self.workspace_ids,
|
||||
"user_token": self.user_token
|
||||
}, out_file, indent = 6)
|
||||
out_file.close()
|
||||
|
||||
def load(self):
|
||||
if not os.path.exists(self.state_file):
|
||||
return
|
||||
|
||||
try:
|
||||
with open(self.state_file) as f:
|
||||
data = json.load(f)
|
||||
try:
|
||||
self.user_id = data['user_id']
|
||||
self.selected_workspace_id = data['selected_workspace_id']
|
||||
self.enable_verbose = data['enable_verbose']
|
||||
self.img2img_include_init_images = data['img2img_include_init_images']
|
||||
self.img2img_include_mask_image = data['img2img_include_mask_image']
|
||||
self.scale_to_standard_size = data['scale_to_standard_size']
|
||||
self.enable_metadata = data['enable_metadata']
|
||||
self.enable_analytics = data['enable_analytics']
|
||||
self.workspace_dd = data['workspace_dd']
|
||||
self.workspace_ids = data['workspace_ids']
|
||||
self.user_token = data['user_token']
|
||||
except KeyError:
|
||||
print('Unsupported state file version, flushing state...')
|
||||
f.close()
|
||||
|
||||
except json.JSONDecodeError:
|
||||
print("JSONDecode Error")
|
||||
return
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
#
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2023 Thought Stream, LLC dba Bluescape.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
from .config import Config
|
||||
|
||||
|
||||
|
||||
def login_endpoint_page(challenge):
|
||||
return f"""
|
||||
<script>
|
||||
const url = `{Config.isam_base_domain}/api/v3/oauth2/authorize?client_id={Config.client_id}&redirect_uri={Config.auth_redirect_url}&response_type=code&scope=v2legacy%20offline_access&state=1a2b3c4d&code_challenge={challenge}&code_challenge_method=S256`;
|
||||
window.location = url;
|
||||
</script>
|
||||
"""
|
||||
|
||||
def registration_endpoint_page(registration_url):
|
||||
return f"""
|
||||
<script>
|
||||
localStorage.setItem("bluescapeRegistrationStarted", true);
|
||||
window.location = '{registration_url}';
|
||||
</script>
|
||||
"""
|
||||
|
||||
def refresh_ui_page():
|
||||
return f"""
|
||||
<script>
|
||||
localStorage.setItem("bluescapeRefreshUI", true);
|
||||
window.location = "/";
|
||||
</script>
|
||||
"""
|
||||
|
||||
bluescape_auth_function = f"""
|
||||
function bluescape_auth() {{
|
||||
window.location = '/bluescape/login';
|
||||
}}
|
||||
"""
|
||||
|
||||
bluescape_open_workspace_function = f"""
|
||||
function open_selected_workspace() {{
|
||||
const selectedWorkspace = document.querySelector("#bluescape-open-workspace-id textarea");
|
||||
if (selectedWorkspace) {{
|
||||
workspaceId = selectedWorkspace.value;
|
||||
const url = '{Config.client_base_domain}' + '/' + workspaceId;
|
||||
window.open(url, "_blank");
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
|
||||
expired_token_message_block = "<span style='color:red;'>Bluescape auth has expired. Please login in the Bluescape tab.</span>"
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
#
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2023 Thought Stream, LLC dba Bluescape.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
from modules.processing import Processed
|
||||
|
||||
def get_image_traits(enable_metadata: bool, processed: Processed, seed, subseed, infotext, generation_type, upload_id):
|
||||
|
||||
if enable_metadata:
|
||||
traits = {
|
||||
"http://bluescape.dev/automatic1111-extension/v1/enabled": True,
|
||||
"http://bluescape.dev/automatic1111-extension/v1/postprocess": str({
|
||||
"type": str(generation_type),
|
||||
"prompt": str(processed.prompt),
|
||||
"negative_prompt": str(processed.negative_prompt),
|
||||
"seed": str(seed),
|
||||
"subseed": str(subseed),
|
||||
"infotext": str(infotext),
|
||||
"subseed_strength": str(processed.subseed_strength),
|
||||
"width": str(processed.width),
|
||||
"height": str(processed.height),
|
||||
"sampler_name": str(processed.sampler_name),
|
||||
"cfg_scale": str(processed.cfg_scale),
|
||||
"image_cfg_scale": str(processed.image_cfg_scale),
|
||||
"restore_faces": str(processed.restore_faces),
|
||||
"face_restoration_model": str(processed.face_restoration_model),
|
||||
"sd_model_hash": str(processed.sd_model_hash),
|
||||
"seed_resize_from_w": str(processed.seed_resize_from_w),
|
||||
"seed_resize_from_h": str(processed.seed_resize_from_h),
|
||||
"denoising_strength": str(processed.denoising_strength),
|
||||
"extra_generation_params": str(processed.extra_generation_params),
|
||||
"clip_skip": str(processed.clip_skip), # missing in infobar
|
||||
"eta": str(processed.eta),
|
||||
"ddim_discretize": str(processed.ddim_discretize),
|
||||
"s_churn": str(processed.s_churn),
|
||||
"s_tmin": str(processed.s_tmin),
|
||||
"s_tmax": str(processed.s_tmax),
|
||||
"s_noise": str(processed.s_noise),
|
||||
"sampler_noise_scheduler_override": str(processed.sampler_noise_scheduler_override),
|
||||
"is_using_inpainting_conditioning": str(processed.is_using_inpainting_conditioning),
|
||||
"upload_id": str(upload_id),
|
||||
}),
|
||||
"http://bluescape.dev/automatic1111-extension/v1/uploadId": str(upload_id),
|
||||
}
|
||||
else:
|
||||
traits = {
|
||||
"http://bluescape.dev/automatic1111-extension/v1/enabled": False,
|
||||
}
|
||||
|
||||
return traits
|
||||
|
||||
def get_canvas_traits(enable_metadata: bool, processed: Processed, generation_type, upload_id, num_images):
|
||||
|
||||
if enable_metadata:
|
||||
traits = {
|
||||
"http://bluescape.dev/automatic1111-extension/v1/enabled": True,
|
||||
"http://bluescape.dev/automatic1111-extension/v1/processed": str({
|
||||
"type": str(generation_type),
|
||||
"prompt": str(processed.prompt),
|
||||
"negative_prompt": str(processed.negative_prompt),
|
||||
# No seed, subseed
|
||||
"subseed_strength": str(processed.subseed_strength),
|
||||
"width": str(processed.width),
|
||||
"height": str(processed.height),
|
||||
"sampler_name": str(processed.sampler_name),
|
||||
"cfg_scale": str(processed.cfg_scale),
|
||||
"image_cfg_scale": str(processed.image_cfg_scale),
|
||||
"restore_faces": str(processed.restore_faces),
|
||||
"face_restoration_model": str(processed.face_restoration_model),
|
||||
"sd_model_hash": str(processed.sd_model_hash),
|
||||
"seed_resize_from_w": str(processed.seed_resize_from_w),
|
||||
"seed_resize_from_h": str(processed.seed_resize_from_h),
|
||||
"denoising_strength": str(processed.denoising_strength),
|
||||
"extra_generation_params": str(processed.extra_generation_params),
|
||||
"clip_skip": str(processed.clip_skip), # missing in infobar
|
||||
"eta": str(processed.eta),
|
||||
"ddim_discretize": str(processed.ddim_discretize),
|
||||
"s_churn": str(processed.s_churn),
|
||||
"s_tmin": str(processed.s_tmin),
|
||||
"s_tmax": str(processed.s_tmax),
|
||||
"s_noise": str(processed.s_noise),
|
||||
"sampler_noise_scheduler_override": str(processed.sampler_noise_scheduler_override),
|
||||
"is_using_inpainting_conditioning": str(processed.is_using_inpainting_conditioning),
|
||||
"num_images": str(num_images),
|
||||
"all_prompts": str(processed.all_prompts),
|
||||
"all_negative_prompts": str(processed.all_negative_prompts),
|
||||
"all_seeds": str(processed.all_seeds),
|
||||
"all_subseeds": str(processed.all_subseeds),
|
||||
"infotexts": str(processed.infotexts),
|
||||
"upload_id": str(upload_id),
|
||||
}),
|
||||
"http://bluescape.dev/automatic1111-extension/v1/uploadId": str(upload_id),
|
||||
}
|
||||
else:
|
||||
traits = {
|
||||
"http://bluescape.dev/automatic1111-extension/v1/enabled": False,
|
||||
}
|
||||
|
||||
return traits
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
#
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2023 Thought Stream, LLC dba Bluescape.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
import launch
|
||||
|
||||
if not launch.is_installed("appdirs"):
|
||||
launch.run_pip("install appdirs==1.4.4", "requirements for Bluescape extension")
|
||||
|
||||
if not launch.is_installed("pkce"):
|
||||
launch.run_pip("install pkce==1.0.3", "requirements for Bluescape extension")
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Thought Stream, LLC dba Bluescape.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
function bluescape_registration() {
|
||||
window.open("/bluescape/registration", "_blank");
|
||||
}
|
||||
|
||||
function bluescape_logout() {
|
||||
window.location = "/bluescape/logout";
|
||||
}
|
||||
|
||||
function bluescape_update_status(input) {
|
||||
const statuses = input.split("\n")
|
||||
const txt2imgStatusLabel = document.getElementById("bluescape-status-txt2img");
|
||||
const img2imgStatusLabel = document.getElementById("bluescape-status-img2img");
|
||||
|
||||
// This is a bit error prone and should be cleaned up
|
||||
if (statuses[0] && txt2imgStatusLabel) {
|
||||
txt2imgStatusLabel.innerHTML = statuses[0]
|
||||
}
|
||||
if (statuses[1] && img2imgStatusLabel) {
|
||||
img2imgStatusLabel.innerHTML = statuses[1]
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const bsAuthSuccess = localStorage.getItem("bluescapeRefreshUI");
|
||||
if (bsAuthSuccess) {
|
||||
localStorage.removeItem("bluescapeRefreshUI");
|
||||
RetryCallback(ClickOnBluescapeTab, 500, 6);
|
||||
}
|
||||
|
||||
// Status polling
|
||||
statusInterval = setInterval(async () => {
|
||||
try {
|
||||
const response = await fetch("/bluescape/status");
|
||||
bluescape_update_status(await response.text());
|
||||
}
|
||||
catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
function RetryCallback(callback, delay, tries) {
|
||||
if (tries && callback() !== true) {
|
||||
setTimeout(RetryCallback.bind(this, callback, delay, tries - 1), delay);
|
||||
}
|
||||
}
|
||||
|
||||
function ClickOnBluescapeTab() {
|
||||
const links = Array.from(document.querySelectorAll('#tabs .tab-nav button'));
|
||||
let clicked = false;
|
||||
links.forEach((link) => {
|
||||
if (link.textContent === "Bluescape ") {
|
||||
link.click();
|
||||
clicked = true;
|
||||
}
|
||||
});
|
||||
|
||||
return clicked;
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 379 KiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 903 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
|
@ -0,0 +1,27 @@
|
|||
#
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2023 Thought Stream, LLC dba Bluescape.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
|
||||
from bs.bluescape_upload import Script
|
||||
|
||||
__all__ = ["Script"]
|
||||
|
|
@ -0,0 +1 @@
|
|||
0.8.1
|
||||