Merge branch 'pythongosssss:main' into main

This commit is contained in:
mijuku233
2024-01-04 13:07:29 +08:00
committed by GitHub
20 changed files with 216 additions and 54 deletions
+3 -1
View File
@@ -1 +1,3 @@
__pycache__
__pycache__
pysssss.json
user/autocomplete.txt
+72
View File
@@ -0,0 +1,72 @@
import torch
import numpy as np
from PIL import Image
class ConstrainImageforVideo:
"""
A node that constrains an image to a maximum and minimum size while maintaining aspect ratio.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
"max_width": ("INT", {"default": 1024, "min": 0}),
"max_height": ("INT", {"default": 1024, "min": 0}),
"min_width": ("INT", {"default": 0, "min": 0}),
"min_height": ("INT", {"default": 0, "min": 0}),
"crop_if_required": (["yes", "no"], {"default": "no"}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("IMAGE",)
FUNCTION = "constrain_image_for_video"
CATEGORY = "image"
def constrain_image_for_video(self, images, max_width, max_height, min_width, min_height, crop_if_required):
crop_if_required = crop_if_required == "yes"
results = []
for image in images:
i = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)).convert("RGB")
current_width, current_height = img.size
aspect_ratio = current_width / current_height
constrained_width = max(min(current_width, min_width), max_width)
constrained_height = max(min(current_height, min_height), max_height)
if constrained_width / constrained_height > aspect_ratio:
constrained_width = max(int(constrained_height * aspect_ratio), min_width)
if crop_if_required:
constrained_height = int(current_height / (current_width / constrained_width))
else:
constrained_height = max(int(constrained_width / aspect_ratio), min_height)
if crop_if_required:
constrained_width = int(current_width / (current_height / constrained_height))
resized_image = img.resize((constrained_width, constrained_height), Image.LANCZOS)
if crop_if_required and (constrained_width > max_width or constrained_height > max_height):
left = max((constrained_width - max_width) // 2, 0)
top = max((constrained_height - max_height) // 2, 0)
right = min(constrained_width, max_width) + left
bottom = min(constrained_height, max_height) + top
resized_image = resized_image.crop((left, top, right, bottom))
resized_image = np.array(resized_image).astype(np.float32) / 255.0
resized_image = torch.from_numpy(resized_image)[None,]
results.append(resized_image)
all_images = torch.cat(results, dim=0)
return (all_images, all_images.size(0),)
NODE_CLASS_MAPPINGS = {
"ConstrainImageforVideo|pysssss": ConstrainImageforVideo,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ConstrainImageforVideo|pysssss": "Constrain Image for Video 🐍",
}
+1 -1
View File
@@ -95,7 +95,7 @@ class MathExpression:
return expression
def get_widget_value(self, extra_pnginfo, prompt, node_name, widget_name):
workflow = extra_pnginfo["workflow"]
workflow = extra_pnginfo["workflow"] if "workflow" in extra_pnginfo else { "nodes": [] }
node_id = None
for node in workflow["nodes"]:
name = node["type"]
+6 -4
View File
@@ -14,21 +14,23 @@ class PlaySound:
return {"required": {
"any": (any, {}),
"mode": (["always", "on empty queue"], {}),
"volume": ("FLOAT", {"min": 0, "max": 1, "step": 0.1, "default": 0.5})
"volume": ("FLOAT", {"min": 0, "max": 1, "step": 0.1, "default": 0.5}),
"file": ("STRING", { "default": "notify.mp3" })
}}
FUNCTION = "nop"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
OUTPUT_NODE = True
RETURN_TYPES = ()
RETURN_TYPES = (any,)
CATEGORY = "utils"
def IS_CHANGED(self, **kwargs):
return float("NaN")
def nop(self, any, mode, volume):
return {"ui": {"a": []}, "result": ()}
def nop(self, any, mode, volume, file):
return {"ui": {"a": []}, "result": (any,)}
NODE_CLASS_MAPPINGS = {
+13 -7
View File
@@ -64,6 +64,8 @@ def get_file(root_dir, file):
root_dir = get_dir_from_name(root_dir)
root_dir = get_real_path(root_dir)
if not os.path.exists(root_dir):
os.mkdir(root_dir)
full_path = os.path.join(root_dir, file)
if not is_child_dir(root_dir, full_path):
@@ -78,10 +80,13 @@ class TextFileNode:
@classmethod
def VALIDATE_INPUTS(self, root_dir, file, **kwargs):
self.file = get_file(root_dir, file)
if file == "[none]" or not file or not file.strip():
return True
get_file(root_dir, file)
return True
def load_text(self, **kwargs):
self.file = get_file(kwargs["root_dir"], kwargs["file"])
with open(self.file, "r") as f:
return (f.read(), )
@@ -168,17 +173,18 @@ class SaveText(TextFileNode):
FUNCTION = "write_text"
def write_text(self, root_dir, file, append, insert, text):
if append == "new only" and os.path.exists(self.file):
def write_text(self, **kwargs):
self.file = get_file(kwargs["root_dir"], kwargs["file"])
if kwargs["append"] == "new only" and os.path.exists(self.file):
raise FileExistsError(
self.file + " already exists and 'new only' is selected.")
with open(self.file, "a+" if append == "append" else "w") as f:
with open(self.file, "a+" if kwargs["append"] == "append" else "w") as f:
is_append = f.tell() != 0
if is_append and insert:
if is_append and kwargs["insert"]:
f.write("\n")
f.write(text)
f.write(kwargs["text"])
return super().load_text()
return super().load_text(**kwargs)
NODE_CLASS_MAPPINGS = {
+14 -4
View File
@@ -70,11 +70,21 @@ def get_extension_config(reload=False):
return config
config_path = get_ext_dir("pysssss.json")
default_config_path = get_ext_dir("pysssss.default.json")
if not os.path.exists(config_path):
log("Missing pysssss.json, this extension may not work correctly. Please reinstall the extension.",
type="ERROR", always=True, name="???")
print(f"Extension path: {get_ext_dir()}")
return {"name": "Unknown", "version": -1}
if os.path.exists(default_config_path):
shutil.copy(default_config_path, config_path)
if not os.path.exists(config_path):
log(f"Failed to create config at {config_path}", type="ERROR", always=True, name="???")
print(f"Extension path: {get_ext_dir()}")
return {"name": "Unknown", "version": -1}
else:
log("Missing pysssss.default.json, this extension may not work correctly. Please reinstall the extension.",
type="ERROR", always=True, name="???")
print(f"Extension path: {get_ext_dir()}")
return {"name": "Unknown", "version": -1}
with open(config_path, "r") as f:
config = json.loads(f.read())
return config
View File
+24 -2
View File
@@ -312,7 +312,7 @@ app.registerExtension({
};
TextAreaAutoComplete.globalSeparator = localStorage.getItem(id + ".AutoSeparate") ?? ", ";
app.ui.settings.addSetting({
const enabledSetting = app.ui.settings.addSetting({
id,
name: "🐍 Text Autocomplete",
defaultValue: true,
@@ -356,7 +356,6 @@ app.registerExtension({
},
[
$el("input", {
id: id.replaceAll(".", "-"),
type: "checkbox",
checked: !!TextAreaAutoComplete.globalSeparator,
onchange: (event) => {
@@ -367,6 +366,26 @@ app.registerExtension({
}),
]
),
$el(
"label",
{
textContent: "Replace _ with space ",
style: {
display: "block",
},
},
[
$el("input", {
type: "checkbox",
checked: !!TextAreaAutoComplete.replacer,
onchange: (event) => {
const checked = !!event.target.checked;
TextAreaAutoComplete.replacer = checked ? (v) => v.replaceAll("_", " ") : undefined;
localStorage.setItem(id + ".ReplaceUnderscore", checked);
},
}),
]
),
$el("button", {
textContent: "Manage Custom Words",
onclick: () => {
@@ -383,6 +402,9 @@ app.registerExtension({
]);
},
});
TextAreaAutoComplete.enabled = enabledSetting.value;
TextAreaAutoComplete.replacer = localStorage.getItem(id + ".ReplaceUnderscore") === "true" ? (v) => v.replaceAll("_", " ") : undefined;
},
beforeRegisterNodeDef(_, def) {
// Process each input to see if there is a custom word list for
+5 -4
View File
@@ -27,10 +27,11 @@ app.registerExtension({
left: 0;
top: 0;
transform: translate(-100%, 0);
width: 256px;
height: 256px;
background-size: cover;
background-position: center;
width: 384px;
height: 384px;
background-size: contain;
background-position: top right;
background-repeat: no-repeat;
filter: brightness(65%);
}
`,
+13 -14
View File
@@ -175,8 +175,9 @@ const CHAR_CODE_ZERO = "0".charCodeAt(0);
const CHAR_CODE_NINE = "9".charCodeAt(0);
class TextAreaCaretHelper {
constructor(el) {
constructor(el, getScale) {
this.el = el;
this.getScale = getScale;
}
#calculateElementOffset() {
@@ -253,11 +254,12 @@ class TextAreaCaretHelper {
}
getCursorOffset() {
const scale = this.getScale();
const elOffset = this.#calculateElementOffset();
const elScroll = this.#getElScroll();
const cursorPosition = this.#getCursorPosition();
const lineHeight = this.#getLineHeightPx();
const top = elOffset.top - elScroll.top + cursorPosition.top + lineHeight;
const top = elOffset.top - (elScroll.top * scale) + (cursorPosition.top + lineHeight) * scale;
const left = elOffset.left - elScroll.left + cursorPosition.left;
const clientTop = this.el.getBoundingClientRect().top;
if (this.el.dir !== "rtl") {
@@ -289,8 +291,7 @@ class TextAreaCaretHelper {
const startPos = this.el.selectionStart;
const endPos = this.el.selectionEnd;
this.el.value =
this.el.value.substring(0, startPos + offset) + value + this.el.value.substring(endPos, this.el.value.length);
this.el.value = this.el.value.substring(0, startPos + offset) + value + this.el.value.substring(endPos, this.el.value.length);
this.el.selectionEnd = this.el.selectionStart = startPos + value.length + offset + (finalOffset ?? 0);
} else {
this.el.value += value;
@@ -313,6 +314,7 @@ class TextAreaCaretHelper {
export class TextAreaAutoComplete {
static globalSeparator = "";
static enabled = true;
static replacer = undefined;
/** @type {Record<string, Record<string, AutoCompleteEntry>>} */
static groups = {};
@@ -341,7 +343,7 @@ export class TextAreaAutoComplete {
*/
constructor(el, words = null, separator = null) {
this.el = el;
this.helper = new TextAreaCaretHelper(el);
this.helper = new TextAreaCaretHelper(el, () => app.canvas.ds.scale);
this.dropdown = $el("div.pysssss-autocomplete");
this.overrideWords = words;
this.overrideSeparator = separator;
@@ -481,10 +483,7 @@ export class TextAreaAutoComplete {
);
const top = priorityMatches.length * 0.2;
return priorityMatches
.slice(0, top)
.concat(prefixMatches, priorityMatches.slice(top), includesMatches)
.slice(0, 20);
return priorityMatches.slice(0, top).concat(prefixMatches, priorityMatches.slice(top), includesMatches).slice(0, 20);
}
#update() {
@@ -567,11 +566,11 @@ export class TextAreaAutoComplete {
{
onclick: () => {
this.el.focus();
this.helper.insertAtCursor(
(wordInfo.value ?? wordInfo.text) + this.separator,
-before.length,
wordInfo.caretOffset
);
let value = wordInfo.value ?? wordInfo.text;
if(TextAreaAutoComplete.replacer) {
value = TextAreaAutoComplete.replacer(value);
}
this.helper.insertAtCursor(value + this.separator, -before.length, wordInfo.caretOffset);
setTimeout(() => {
this.#update();
}, 150);
+31 -1
View File
@@ -80,6 +80,12 @@ export class Lightbox {
}
async update(shift) {
if (shift < 0 && this.index <= 0) {
return;
}
if (shift > 0 && this.index >= this.images.length - 1) {
return;
}
this.index += shift;
this.prev.style.visibility = this.index ? "unset" : "hidden";
@@ -91,7 +97,11 @@ export class Lightbox {
this.el.style.opacity = 1;
this.img.style.opacity = 0;
this.spinner.style.display = "inline-block";
await loadImage(img);
try {
await loadImage(img);
} catch (err) {
console.error('failed to load image', img, err);
}
this.spinner.style.display = "none";
this.link.href = img;
this.img.src = img;
@@ -100,3 +110,23 @@ export class Lightbox {
}
export const lightbox = new Lightbox();
addEventListener('keydown', (event) => {
if (lightbox.el.style.display === 'none') {
return;
}
const { key } = event;
switch (key) {
case 'ArrowLeft':
case 'a':
lightbox.update(-1);
break;
case 'ArrowRight':
case 'd':
lightbox.update(1);
break;
case 'Escape':
lightbox.close();
break;
}
});
+1 -1
View File
@@ -410,7 +410,7 @@ app.registerExtension({
api.addEventListener("executed", ({ detail }) => {
if (visible && detail?.output?.images) {
for (const src of detail.output.images) {
const href = `/view?filename=${encodeURIComponent(src.filename)}&type=${
const href = `./view?filename=${encodeURIComponent(src.filename)}&type=${
src.type
}&subfolder=${encodeURIComponent(src.subfolder)}&t=${+new Date()}`;
+2
View File
@@ -234,6 +234,8 @@ const infoHandler = {
CheckpointLoader: "checkpoints",
CheckpointLoaderSimple: "checkpoints",
"CheckpointLoader|pysssss": "checkpoints",
"Efficient Loader": "checkpoints",
"Eff. Loader SDXL": "checkpoints",
};
app.registerExtension({
+12 -1
View File
@@ -15,7 +15,18 @@ app.registerExtension({
return;
}
}
const url = new URL(`assets/notify.mp3`, import.meta.url);
let file = this.widgets[2].value;
if (!file) {
file = "notify.mp3";
}
if (!file.startsWith("http")) {
if (!file.includes("/")) {
file = "assets/" + file;
}
file = new URL(file, import.meta.url)
}
const url = new URL(file);
const audio = new Audio(url);
audio.volume = this.widgets[1].value;
audio.play();
+1 -1
View File
@@ -154,7 +154,7 @@ app.registerExtension({
v = this.widgets_values[0];
}
let config = [link.type, {}];
if (input.widget) {
if (input.widget?.config) {
config = input.widget.config;
}
const { widget } = ComfyWidgets[widgetType](this, "value", config, app);
+6 -4
View File
@@ -49,11 +49,13 @@ const ext = {
api.addEventListener("executed", ({ detail }) => {
if (!enabled) return;
const images = detail?.output?.images;
if (!images) return;
if (!images || !images.length) return;
const format = app.getPreviewFormatParam();
const src = `/view?filename=${encodeURIComponent(images[0].filename)}&type=${
images[0].type
}&subfolder=${encodeURIComponent(images[0].subfolder)}&t=${+new Date()}${format}`;
const src = [
`./view?filename=${encodeURIComponent(images[0].filename)}`,
`type=${images[0].type}`,
`subfolder=${encodeURIComponent(images[0].subfolder)}`,
`t=${+new Date()}${format}`,].join('&');
show(src, detail.node);
});
+8 -8
View File
@@ -2,23 +2,23 @@ import { app } from "../../../scripts/app.js";
import { ComfyWidgets } from "../../../scripts/widgets.js";
// Displays input text on a node
app.registerExtension({
name: "pysssss.ShowText",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "ShowText|pysssss") {
function populate(text) {
if (this.widgets) {
const pos = this.widgets.findIndex((w) => w.name === "text");
if (pos !== -1) {
for (let i = pos; i < this.widgets.length; i++) {
this.widgets[i].onRemove?.();
}
this.widgets.length = pos;
for (let i = 1; i < this.widgets.length; i++) {
this.widgets[i].onRemove?.();
}
this.widgets.length = 1;
}
for (const list of text) {
const v = [...text];
if (!v[0]) {
v.shift();
}
for (const list of v) {
const w = ComfyWidgets["STRING"](this, "text", ["STRING", { multiline: true }], app).widget;
w.inputEl.readOnly = true;
w.inputEl.style.opacity = 0.6;
+1 -1
View File
@@ -386,7 +386,7 @@ class SvgWorkflowImage extends WorkflowImage {
return;
} else if (file && (file.type === "image/jpeg" || file.name?.endsWith(".jpg") || file.name?.endsWith(".jpeg"))) {
if (
await new Promise((r) => {
await new Promise((resolve) => {
try {
// This shouldnt go in here but it's easier than refactoring handleFile
const reader = new FileReader();
+3
View File
@@ -81,6 +81,9 @@ async function saveWorkflow(name, workflow, overwrite) {
class PysssssWorkflows {
async load() {
this.workflows = await getWorkflows();
if(this.workflows.length) {
this.workflows.sort();
}
this.loadMenu.style.display = this.workflows.length ? "flex" : "none";
}