Merge branch 'pythongosssss:main' into main
This commit is contained in:
+3
-1
@@ -1 +1,3 @@
|
||||
__pycache__
|
||||
__pycache__
|
||||
pysssss.json
|
||||
user/autocomplete.txt
|
||||
@@ -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 🐍",
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
+24
-2
@@ -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
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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()}`;
|
||||
|
||||
|
||||
@@ -234,6 +234,8 @@ const infoHandler = {
|
||||
CheckpointLoader: "checkpoints",
|
||||
CheckpointLoaderSimple: "checkpoints",
|
||||
"CheckpointLoader|pysssss": "checkpoints",
|
||||
"Efficient Loader": "checkpoints",
|
||||
"Eff. Loader SDXL": "checkpoints",
|
||||
};
|
||||
|
||||
app.registerExtension({
|
||||
|
||||
+12
-1
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user