Skip to content

Commit

Permalink
initial setup
Browse files Browse the repository at this point in the history
  • Loading branch information
userna committed Apr 11, 2024
1 parent bc18bcf commit ac512b9
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 3 deletions.
12 changes: 12 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Welcome to submit a Pull Request to include new scripts into the Draw Things app!

To submit the Pull Request, please make sure to include the following items in the description:

1. License for the new script;

2. Recommended settings / parameters to use with the script;

3. Self-reported age rating, following this: <https://developer.apple.com/help/app-store-connect/reference/age-ratings/>.

Thanks for your contribution!

19 changes: 19 additions & 0 deletions .github/workflows/auto-assign.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Auto Assign
on:
issues:
types: [opened]
pull_request:
types: [opened]
jobs:
run:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: 'Auto-assign issue'
uses: pozil/auto-assign-issue@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
assignees: liuliu
numOfAssignee: 1
43 changes: 43 additions & 0 deletions .github/workflows/json.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: json
on:
push:
branches:
- main

permissions:
contents: write

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Run JSON generation.
run: |
python3 scripts/script_json.py
git config --global user.email "[email protected]"
git config --global user.name "docbot"
git commit -a -m "Update metadata with new script." || true
git pull --rebase && git push || true
- name: Clean up json branch
run: |
git branch -D json || true
git checkout -b json
mkdir -p docs
mkdir -p docs/scripts
mv *.json ./docs
find scripts -type f -name "*.js" -exec cp {} ./docs/scripts \;
echo "scripts.drawthings.ai" > ./docs/CNAME
- name: Add and commit documentation
run: |
git add "docs/*" && git commit -m "Update docs."
- name: Push the new branch
run: |
git push --force origin json:json
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@ This repository maintains the source material and publish mechanism for "Communi

TBD

You are welcome to put up Pull Requests to add new scripts to the "Community" section within the app. To add a new model, first create a directory under either `./models` or `./loras` and add the following files:
You are welcome to put up Pull Requests to add new scripts to the "Community" section within the app. To add a new script, first create a directory under `./scripts` and add the following files:

1. a `metadata.json` file contains the metadata for both the source link (must be public-available) and enough metadata to be used within the app. There are some examples in the directory;
1. add LICENSE.md

2. add the js file

3. Add a `metadata.json` file that contains the metadata including name and other information. If metadata.json is not provided, the default metadata will be generated based on the name of the js file.

Then add the directory name for your script into the `scripts.txt` file at the top level of the repository.

# How It Works

Once a Pull Request merged into the repository, an automatic process will be kicked off to add the scripts to a listing hosted by Draw Things. A new json list will be generated so the script will be available to everyone use the app upon a refresh.

# License

All materials in this repository are published under [CC0 1.0 Universal](https://creativecommons.org/public-domain/cc0/) otherwise known as "Public Domain".
TBD
6 changes: 6 additions & 0 deletions scripts-metadata-template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"display_name": "SD Ultimate Upscale",
"author": "Draw Things",
"file": "sd-ultimate-upscale.js",
"description": "Generate high-quality, high-resolution images from low-resolution inputs, while preserving fine details and textures."
}
1 change: 1 addition & 0 deletions scripts.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sd-ultimate-upscale
6 changes: 6 additions & 0 deletions scripts/sd-ultimate-upscale/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"display_name": "SD Ultimate Upscale",
"author": "Draw Things",
"file": "sd-ultimate-upscale.js",
"description": "Generate high-quality, high-resolution images from low-resolution inputs, while preserving fine details and textures."
}
194 changes: 194 additions & 0 deletions scripts/sd-ultimate-upscale/sd-ultimate-upscale.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
//@api-1.0
//
// "Scripts" is a way to enhance Draw Things experience with custom JavaScript snippets. Over time,
// we will enhance Scripts with more APIs and do neccessary modifications on the said APIs, but we
// will also keep the API stable. In particular, you can mark your script with a particular API
// version and it will make sure we use the proper API toolchain to execute the script even in
// the future some of the APIs changed.
//
// Existing APIs:
//
// We currently provides three global objects for you to interact with: `canvas`, `pipeline` and `filesystem`.
//
// `canvas` is the object to manipulate the existing infinite canvas on Draw Things interface. It
// supports the following methods:
//
// `canvas.boundingBox`: return the bounding box of existing images collectively on the canvas.
// `canvas.clear()`: remove all images on the canvas, same effect as you tap on the "New Canvas" button.
// `canvas.createMask(width, height, value)`: create a mask with a given size and fill-in value.
// `canvas.currentMask`: return the current mask on the view, you can further manipulate the mask through `Mask` APIs.
// `canvas.loadImage(file)`: load image from a file (a path).
// `canvas.moveCanvas(left, top)`: move the canvas to a particular coordinate, these coordinates work as if the zoom scale is 1x.
// `canvas.saveImage(file, visibleRegionOnly)`: save the image currently on canvas to a file.
// `canvas.canvasZoom`: Set / get the zoom scale of the canvas.
// `canvas.topLeftCorner`: return the coordinate the canvas currently at, these coordinates work as if the zoom scale is 1x.
// `canvas.updateCanvasSize(configuration)`: extract the size from configuration object and then sync the canvas with that size.
//
// `pipeline` is the object to run the image generation pipeline. It supports the following methods:
//
// `pipeline.configuration`: extract the current configuration, whether it is on screen or after pipeline.run.
// `pipeline.findControlByName(name)`: using the display name to find a particular control.
// `pipeline.findLoRAByName(name)`: using the display name to find a particular LoRA.
// `pipeline.prompts`: return prompt and negativePrompt currently on the screen.
// `pipeline.run({prompt: null, negativePrompt: null, configuration: configuration, mask: null})`: run image generation through
// this API. You can optionally provides prompt / negativePrompt, or it can take prompt from the screen. mask can be provided
// optionally too and sometimes it can be helpful.
//
// `filesystem` provides a simple access to the underlying file system. Note that Draw Things is
// sandboxed so we can only have access to particular directories within the user file system.
//
// `filesystem.pictures.path`: get the path of the pictures folder. It is the system Pictures folder on macOS, and a Pictures
// folder under Draw Things within Files app for iOS / iPadOS.
// `filesystem.pictures.readEntries`: enumerate all the files under the pictures folder.
//
// This script upscales the original image to 512x512 tiles and fixes the seams.
// This script assumes the original image is at 1x zoom level.
// The configuration parameters in this script may need tuning to improve final image.
// Author: @Gooster

const USE_UPSCALER = "Real-ESRGAN X2+";
const UPSCALE_FACTOR = 2;

const TILE_STRENGTH = 0.7;
const INPAINTING_STRENGTH = 0.7;
const MASK_BLUR = 6;
const MIN_OVERLAP = 48;
const TILE_SIZE = 512;

const configuration = pipeline.configuration;
configuration.maskBlur = MASK_BLUR;

const imageRect = canvas.boundingBox;

// initial upscale
canvas.canvasZoom = 1;
if (USE_UPSCALER) {
pipeline.downloadBuiltin(USE_UPSCALER);
configuration.strength = 0;
configuration.upscaler = USE_UPSCALER;
canvas.moveCanvas(imageRect.x, imageRect.y);
// generate the upscaled image
pipeline.run({ configuration: configuration });
}
pipeline.downloadBuiltin("controlnet_tile_1.x_v1.1_f16.ckpt");
pipeline.downloadBuiltin("controlnet_inpaint_1.x_v1.1_f16.ckpt");
// tiling over the image to upscale
configuration.strength = TILE_STRENGTH;
tiledUpscale(configuration, imageRect, UPSCALE_FACTOR);
canvas.moveCanvas(imageRect.x, imageRect.y);
canvas.canvasZoom = 1;

// tiled upscale support integer zoom levels
function tiledUpscale(configuration, imageRect, zoom) {
const minOverlap = MIN_OVERLAP;
// set zoom level and reconfigure the canvas dimensions to tile size
configuration.upscaler = null;
configuration.width = configuration.height = TILE_SIZE;
canvas.updateCanvasSize(configuration);
canvas.canvasZoom = zoom;
canvas.moveCanvas(imageRect.x, imageRect.y);
const numTiles = {
x: imageRect.width * zoom / configuration.width,
y: imageRect.height * zoom / configuration.height
}
const tileSize = Math.floor(imageRect.width / numTiles.x)
numTiles.x = Math.ceil(numTiles.x + (minOverlap * (numTiles.x - 1)) / configuration.width)
numTiles.y = Math.ceil(numTiles.y + (minOverlap * (numTiles.y - 1)) / configuration.height)
console.log(`Number of tiles: ${numTiles.x * numTiles.y}`);
canvas.moveCanvas(imageRect.x, imageRect.y);
const generatedRows = [];
let previousCoordinates = { x: imageRect.x, y: imageRect.y };
let currentCoordinates = previousCoordinates;

// tiling over the image
for (let y = 0; y < numTiles.y; y++) {
let currentRow = new Rectangle(0, 0, 0, 0)
for (let x = 0; x < numTiles.x; x++) {
// restore tile control and strength for each tile
const tileControl = pipeline.findControlByName("Tile (SD v1.x, ControlNet 1.1)");
configuration.controls = [tileControl];
configuration.strength = TILE_STRENGTH;
currentCoordinates = {
x: imageRect.x + Math.floor(x * (tileSize - minOverlap)),
y: imageRect.y + Math.floor(y * (tileSize - minOverlap))
};
if (x == 0) {
currentRow.x = currentCoordinates.x;
currentRow.y = currentCoordinates.y;
}
// the last tile should match the x, y edge of the image
if (x == numTiles.x - 1) currentCoordinates.x = imageRect.x + imageRect.width - tileSize;
if (y == numTiles.y - 1) currentCoordinates.y = imageRect.y + imageRect.height - tileSize;
console.log(`Upscaling tile ${x + y * numTiles.x} of ${numTiles.x * numTiles.y}`);
// add mask for tiles other than the first
let mask = new Rectangle(currentCoordinates.x, currentCoordinates.y, tileSize, tileSize);
if (x != 0) {
// exclude the current row
mask = mask.exclude(currentRow);
}
if (y != 0) {
// exclude the row above
mask = mask.exclude(generatedRows[y - 1]);
}
// convert mask coordinate to tile local coordinates
mask.x -= currentCoordinates.x;
mask.y -= currentCoordinates.y;
mask.scale(configuration.width / tileSize);
// ensure mask is the same size as the configuration
const generateRegion = canvas.createMask(configuration.width, configuration.height, 0);
generateRegion.fillRectangle(
mask.x,
mask.y,
mask.width,
mask.height, 2);
canvas.moveCanvas(currentCoordinates.x, currentCoordinates.y)
pipeline.run({ configuration: configuration, mask: generateRegion });
// seams are created at the right and bottom edge of each previous tile we can fix the seam at the next tile generation
configuration.strength = INPAINTING_STRENGTH;
const inpaintControl = pipeline.findControlByName("Inpainting (SD v1.x, ControlNet 1.1)");
configuration.controls = [inpaintControl];
const inpaintRegion = canvas.createMask(configuration.width, configuration.height, 0);
let edgePolisher = 5;
if (y > 0) {
// after the first row always fill the previous row's bottom edge
edgePolisher = x === 0 ? 0 : 5;
const inpaintRect = new Rectangle(
edgePolisher,
mask.y - Math.floor(minOverlap / 2),
configuration.width - edgePolisher,
minOverlap);
inpaintRegion.fillRectangle(
inpaintRect.x,
inpaintRect.y,
inpaintRect.width,
inpaintRect.height,
2
)
}
if (x > 0) {
edgePolisher = y === 0 ? 0 : 5;
const inpaintRect = new Rectangle(
mask.x - Math.floor(minOverlap / 2),
edgePolisher,
minOverlap,
configuration.height - edgePolisher);
inpaintRegion.fillRectangle(
inpaintRect.x,
inpaintRect.y,
inpaintRect.width,
inpaintRect.height,
2
)
}
// skip over the first tile that doesn't have seams yet
canvas.moveCanvas(currentCoordinates.x, currentCoordinates.y)
if (x != 0 || y != 0) {
console.log(`Inpainting seams on tile ${x + y * numTiles.x} of ${numTiles.x * numTiles.y}`);
pipeline.run({ configuration: configuration, mask: inpaintRegion });
}
previousCoordinates = currentCoordinates;
currentRow = Rectangle.union(currentRow, new Rectangle(currentCoordinates.x, currentCoordinates.y, tileSize, tileSize));
}
generatedRows.push(currentRow);
}
}
44 changes: 44 additions & 0 deletions worker-scripts/scripts_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import copy
import subprocess
import os
import json

def sha256sum(file):
result = subprocess.run(['sha256sum', file], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if result.returncode == 0:
return result.stdout.strip().split()[0]

def collect_metadata_from_list(file_path):
metadata_array = []
sha256_dict = {}
with open(file_path, 'r') as file:
# Read each line in the file
for line in file:
directory_path = line.strip() # Remove newline and whitespace
print(directory_path)
metadata_path = os.path.join('scripts', directory_path, 'metadata.json')
# process the folders with metadata
if os.path.exists(metadata_path):
# Open and load the JSON content
with open(metadata_path, 'r') as json_file:
metadata = json.load(json_file)
js_file_path = os.path.join('scripts', directory_path, metadata['file'])
if os.path.exists(js_file_path):
sha = sha256sum(js_file_path)
metadata_array.append(copy.deepcopy(metadata))
sha256_dict[directory_path] = sha


return metadata_array, sha256_dict

# Replace 'scripts.txt' with the path to your actual file if it's located elsewhere
scripts_txt_path = 'scripts.txt'
metadata_array, sha256_dict = collect_metadata_from_list(scripts_txt_path)
with open('scripts_sha256.json', 'w') as file:
sha256_string = json.dumps(sha256_dict, indent=2)
file.write(sha256_string)

# Convert the array to a JSON string for printing or further processing
with open('scripts.json', 'w') as file:
metadata_json_string = json.dumps(metadata_array, indent=2)
file.write(metadata_json_string)

0 comments on commit ac512b9

Please sign in to comment.