added preview widgets

This commit is contained in:
SayanoAI
2024-05-27 19:40:15 -04:00
parent 8e3053e482
commit 97f5e8b2b7
9 changed files with 1301 additions and 371 deletions
-156
View File
@@ -1,156 +0,0 @@
import { app } from "../../../scripts/app.js";
import { api } from '../../../scripts/api.js'
function fitHeight(node) {
node.setSize([node.size[0], node.computeSize([node.size[0], node.size[1]])[1]])
node?.graph?.setDirtyCanvas(true);
}
function chainCallback(object, property, callback) {
if (object == undefined) {
//This should not happen.
console.error("Tried to add callback to non-existant object")
return;
}
if (property in object) {
const callback_orig = object[property]
object[property] = function () {
const r = callback_orig.apply(this, arguments);
callback.apply(this, arguments);
return r
};
} else {
object[property] = callback;
}
}
function addPreviewOptions(nodeType) {
chainCallback(nodeType.prototype, "getExtraMenuOptions", function(_, options) {
// The intended way of appending options is returning a list of extra options,
// but this isn't used in widgetInputs.js and would require
// less generalization of chainCallback
let optNew = []
try {
const previewWidget = this.widgets.find((w) => w.name === "audiopreview");
let url = null
if (previewWidget.audioEl?.hidden == false && previewWidget.audioEl.src) {
//Use full quality audio
//url = api.apiURL('/view?' + new URLSearchParams(previewWidget.value.params));
url = previewWidget.audioEl.src
}
if (url) {
optNew.push(
{
content: "Open preview",
callback: () => {
window.open(url, "_blank")
},
},
{
content: "Save preview",
callback: () => {
const a = document.createElement("a");
a.href = url;
a.setAttribute("download", new URLSearchParams(previewWidget.value.params).get("filename"));
document.body.append(a);
a.click();
requestAnimationFrame(() => a.remove());
},
}
);
}
if(options.length > 0 && options[0] != null && optNew.length > 0) {
optNew.push(null);
}
options.unshift(...optNew);
} catch (error) {
console.log(error);
}
});
}
function previewAudio(node,file,type){
var element = document.createElement("div");
const previewNode = node;
var previewWidget = node.addDOMWidget("audiopreview", "preview", element, {
serialize: false,
hideOnZoom: false,
getValue() {
return element.value;
},
setValue(v) {
element.value = v;
},
});
previewWidget.computeSize = function(width) {
if (this.aspectRatio && !this.parentEl.hidden) {
let height = (previewNode.size[0]-20)/ this.aspectRatio + 10;
if (!(height > 0)) {
height = 0;
}
this.computedHeight = height + 10;
return [width, height];
}
return [width, -4];//no loaded src, widget should not display
}
// element.style['pointer-events'] = "none"
previewWidget.value = {hidden: false, paused: false, params: {}}
previewWidget.parentEl = document.createElement("div");
previewWidget.parentEl.className = "audio_preview";
previewWidget.parentEl.style['width'] = "100%"
element.appendChild(previewWidget.parentEl);
previewWidget.audioEl = document.createElement("audio");
previewWidget.audioEl.controls = true;
previewWidget.audioEl.loop = false;
previewWidget.audioEl.muted = false;
previewWidget.audioEl.style['width'] = "100%"
previewWidget.audioEl.addEventListener("loadedmetadata", () => {
previewWidget.aspectRatio = previewWidget.audioEl.audioWidth / previewWidget.audioEl.audioHeight;
fitHeight(this);
});
previewWidget.audioEl.addEventListener("error", () => {
//TODO: consider a way to properly notify the user why a preview isn't shown.
previewWidget.parentEl.hidden = true;
fitHeight(this);
});
let params = {
"filename": file,
"type": type,
}
previewWidget.parentEl.hidden = previewWidget.value.hidden;
// previewWidget.audioEl.autoplay = !previewWidget.value.paused && !previewWidget.value.hidden;
let target_width = 256
if (element.style?.width) {
//overscale to allow scrolling. Endpoint won't return higher than native
target_width = element.style.width.slice(0,-2)*2;
}
if (!params.force_size || params.force_size.includes("?") || params.force_size == "Disabled") {
params.force_size = target_width+"x?"
} else {
let size = params.force_size.split("x")
let ar = parseInt(size[0])/parseInt(size[1])
params.force_size = target_width+"x"+(target_width/ar)
}
previewWidget.audioEl.src = api.apiURL('/view?' + new URLSearchParams(params));
previewWidget.audioEl.hidden = false;
previewWidget.parentEl.appendChild(previewWidget.audioEl)
}
app.registerExtension({
name: "RVC-Studio.PreviewAudio",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData?.name == "PreviewAudio" && nodeData.category?.includes("RVC")) {
console.log({nodeData})
nodeType.prototype.onExecuted = function (data) {
previewAudio(this, data.audio[0], data.audio[1]);
console.log({data})
}
addPreviewOptions(nodeType)
}
}
});
+366
View File
@@ -0,0 +1,366 @@
import { app } from "../../../scripts/app.js";
import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from "../../../scripts/widgets.js"
function fitHeight(node) {
node.setSize([node.size[0], node.computeSize([node.size[0], node.size[1]])[1]])
node?.graph?.setDirtyCanvas(true);
}
function addPreviewWidget(nodeType, nodeData, widgetName="audio", when="onNodeCreated") {
switch (when){
case "onNodeCreated":
// TODO: not working
// chainCallback(nodeType.prototype, "onNodeCreated", function () {
// const audioWidget = this.widgets.find((w) => w.name === widgetName);
// if (!audioWidget || !audioWidget?.value) return null
// const widgetId = `opt_widget-${Math.random()}`
// let options = {
// widgetId, type: "input", filename: audioWidget.value
// }
// nodeData.input.hidden = {
// ...nodeData.input.hidden,
// [widgetId]: ["AUDIOPREVIEW",options]
// };
// previewAudio(this,options)
// audioWidget.callback = function () {
// options = {
// widgetId, type: "input", filename: audioWidget.value
// }
// previewAudio(this,options)
// if (cb) {
// return cb.apply(this, arguments);
// }
// };
// })
break;
case "onExecuted":
chainCallback(nodeType.prototype, "onExecuted", function (data) {
const widgetId = `opt_widget-${Math.random()}`
nodeData.input.hidden = {
...nodeData.input.hidden,
[widgetId]: ["AUDIOPREVIEW",{widgetName,...data.preview[0]}]
};
previewAudio(this,data.preview[0])
})
break;
default: console.log({nodeType, nodeData, widgetName, when, node: this})
}
}
function previewAudio(node,options={filename: null, type: "input", widgetName: "audio", widgetId: null}){
// while (node.widgets.length > 2){
// node.widgets.pop();
// }
try {
console.log({node, options})
var el = document.getElementById(options.widgetId);
el?.remove();
} catch (error) {
console.log(error);
}
var element = document.createElement("div");
element.id = options.widgetId
const previewNode = node;
var previewWidget = node.addDOMWidget("audiopreview", "preview", element, {
serialize: false,
hideOnZoom: false,
getValue() {
return element.value;
},
setValue(v) {
element.value = v;
},
});
previewWidget.computeSize = function(width) {
if (node.aspectRatio && !node.parentEl.hidden) {
let height = (previewNode.size[0]-20)/ node.aspectRatio + 10;
if (!(height > 0)) {
height = 0;
}
node.computedHeight = height + 10;
return [width, height];
}
return [width, -4];//no loaded src, widget should not display
}
// element.style['pointer-events'] = "none"
previewWidget.value = {hidden: false, paused: false, params: {}}
previewWidget.parentEl = document.createElement("div");
previewWidget.parentEl.className = "audio_preview";
previewWidget.parentEl.style['width'] = "100%"
element.appendChild(previewWidget.parentEl);
previewWidget.audioEl = document.createElement("audio");
previewWidget.audioEl.controls = true;
previewWidget.audioEl.loop = false;
previewWidget.audioEl.muted = false;
previewWidget.audioEl.style['width'] = "100%"
previewWidget.audioEl.addEventListener("loadedmetadata", () => {
previewWidget.aspectRatio = previewWidget.audioEl.audioWidth / previewWidget.audioEl.audioHeight;
fitHeight(node);
});
previewWidget.audioEl.addEventListener("error", () => {
//TODO: consider a way to properly notify the user why a preview isn't shown.
previewWidget.parentEl.hidden = true;
fitHeight(node);
});
let params = options;
previewWidget.parentEl.hidden = previewWidget.value.hidden;
previewWidget.audioEl.autoplay = !previewWidget.value.paused && !previewWidget.value.hidden && Boolean(options.autoplay);
let target_width = 256
if (element.style?.width) {
//overscale to allow scrolling. Endpoint won't return higher than native
target_width = element.style.width.slice(0,-2)*2;
}
if (!params.force_size || params.force_size.includes("?") || params.force_size == "Disabled") {
params.force_size = target_width+"x?"
} else {
let size = params.force_size.split("x")
let ar = parseInt(size[0])/parseInt(size[1])
params.force_size = target_width+"x"+(target_width/ar)
}
previewWidget.audioEl.src = api.apiURL('/view?' + new URLSearchParams(params));
previewWidget.audioEl.hidden = false;
previewWidget.parentEl.appendChild(previewWidget.audioEl)
return previewWidget
}
function audioPreview(node, inputName, inputData, app) {
const options = inputData.pop()
const audioWidget = node.widgets.find((w) => w.name === options.widgetName);
if (!audioWidget) return null
/*
A method that returns the required style for the html
*/
var default_value = audioWidget.value;
Object.defineProperty(audioWidget, "value", {
set : function(value) {
this._real_value = value;
},
get : function() {
let value = "";
if (this._real_value) {
value = this._real_value;
} else {
return default_value;
}
if (value.filename) {
let real_value = value;
value = "";
if (real_value.subfolder) {
value = real_value.subfolder + "/";
}
value += real_value.filename;
if(real_value.type && real_value.type !== "input")
value += ` [${real_value.type}]`;
}
return value;
}
});
let element = previewAudio(node, {filename: audioWidget.value, ...options});
const cb = node.callback;
audioWidget.callback = function () {
element = previewAudio(node, {filename: audioWidget.value, ...options});
if (cb) {
return cb.apply(this, arguments);
}
};
// document.body.append(element);
// console.log({element})
// let uploadWidget = this.addWidget(element);
// uploadWidget.options.serialize = false;
return { widget: element };
}
function chainCallback(object, property, callback) {
if (object == undefined) {
//This should not happen.
console.error("Tried to add callback to non-existant object")
return;
}
if (property in object) {
const callback_orig = object[property]
object[property] = function () {
const r = callback_orig.apply(this, arguments);
callback.apply(this, arguments);
return r
};
} else {
object[property] = callback;
}
}
async function uploadFile(file) {
//TODO: Add uploaded file to cache with Cache.put()?
try {
// Wrap file in formdata so it includes filename
const body = new FormData();
const i = file.webkitRelativePath.lastIndexOf('/');
const subfolder = file.webkitRelativePath.slice(0,i+1)
const new_file = new File([file], file.name, {
type: file.type,
lastModified: file.lastModified,
});
body.append("image", new_file);
if (i > 0) {
body.append("subfolder", subfolder);
}
const resp = await api.fetchApi("/upload/image", {
method: "POST",
body,
});
if (resp.status === 200) {
return resp.status
} else {
alert(resp.status + " - " + resp.statusText);
}
} catch (error) {
alert(error);
}
}
function addUploadWidget(nodeType, nodeData, widgetName, type="video") {
chainCallback(nodeType.prototype, "onNodeCreated", function() {
const pathWidget = this.widgets.find((w) => w.name === widgetName);
const fileInput = document.createElement("input");
chainCallback(nodeType.prototype, "onRemoved", () => {
fileInput?.remove();
});
if (type == "folder") {
Object.assign(fileInput, {
type: "file",
style: "display: none",
webkitdirectory: true,
onchange: async () => {
const directory = fileInput.files[0].webkitRelativePath;
const i = directory.lastIndexOf('/');
if (i <= 0) {
throw "No directory found";
}
const path = directory.slice(0,directory.lastIndexOf('/'))
if (pathWidget.options.values.includes(path)) {
alert("A folder of the same name already exists");
return;
}
let successes = 0;
for(const file of fileInput.files) {
if (await uploadFile(file) == 200) {
successes++;
} else {
//Upload failed, but some prior uploads may have succeeded
//Stop future uploads to prevent cascading failures
//and only add to list if an upload has succeeded
if (successes > 0) {
break
} else {
return;
}
}
}
pathWidget.options.values.push(path);
pathWidget.value = path;
if (pathWidget.callback) {
pathWidget.callback(path)
}
},
});
} else if (type == "video") {
Object.assign(fileInput, {
type: "file",
accept: "video/webm,video/mp4,video/mkv,image/gif",
style: "display: none",
onchange: async () => {
if (fileInput.files.length) {
if (await uploadFile(fileInput.files[0]) != 200) {
//upload failed and file can not be added to options
return;
}
const filename = fileInput.files[0].name;
pathWidget.options.values.push(filename);
pathWidget.value = filename;
if (pathWidget.callback) {
pathWidget.callback(filename)
}
}
},
});
} else if (type == "audio") {
Object.assign(fileInput, {
type: "file",
accept: "audio/mpeg,audio/wav,audio/x-wav,audio/ogg",
style: "display: none",
onchange: async () => {
if (fileInput.files.length) {
if (await uploadFile(fileInput.files[0]) != 200) {
//upload failed and file can not be added to options
return;
}
const filename = fileInput.files[0].name;
pathWidget.options.values.push(filename);
pathWidget.value = filename;
if (pathWidget.callback) {
pathWidget.callback(filename)
}
previewAudio(this, {filename, type: "input", widgetId: filename})
}
},
});
}else {
throw "Unknown upload type"
}
document.body.append(fileInput);
let uploadWidget = this.addWidget("button", "choose " + type + " to upload", "image", () => {
//clear the active click event
app.canvas.node_widget = null
fileInput.click();
});
uploadWidget.options.serialize = false;
});
}
// ComfyWidgets.AUDIOPREVIEW = audioPreview;
app.registerExtension({
name: "RVC-Studio.UI",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.category?.includes("RVC")){
switch (nodeData?.name){
case "LoadAudio":
addUploadWidget(nodeType, nodeData, "audio", "audio")
addPreviewWidget(nodeType, nodeData, "audio", "onNodeCreated" )
break
case "PreviewAudio":
addPreviewWidget(nodeType, nodeData, "audio", "onExecuted" )
break
case "MergeAudioNode":
addPreviewWidget(nodeType, nodeData, "audio", "onExecuted" )
break;
case "RVCNode":
addPreviewWidget(nodeType, nodeData, "audio", "onExecuted" )
break;
default:
break
}
}
},
});
-203
View File
@@ -1,203 +0,0 @@
import { app } from "../../../scripts/app.js";
import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from "../../../scripts/widgets.js"
function fitHeight(node) {
node.setSize([node.size[0], node.computeSize([node.size[0], node.size[1]])[1]])
node?.graph?.setDirtyCanvas(true);
}
function previewAudio(node,file){
while (node.widgets.length > 2){
node.widgets.pop();
}
try {
var el = document.getElementById("uploadAudio");
el?.remove();
} catch (error) {
console.log(error);
}
var element = document.createElement("div");
element.id = "uploadAudio";
const previewNode = node;
var previewWidget = node.addDOMWidget("audiopreview", "preview", element, {
serialize: false,
hideOnZoom: false,
getValue() {
return element.value;
},
setValue(v) {
element.value = v;
},
});
previewWidget.computeSize = function(width) {
if (this.aspectRatio && !this.parentEl.hidden) {
let height = (previewNode.size[0]-20)/ this.aspectRatio + 10;
if (!(height > 0)) {
height = 0;
}
this.computedHeight = height + 10;
return [width, height];
}
return [width, -4];//no loaded src, widget should not display
}
// element.style['pointer-events'] = "none"
previewWidget.value = {hidden: false, paused: false, params: {}}
previewWidget.parentEl = document.createElement("div");
previewWidget.parentEl.className = "audio_preview";
previewWidget.parentEl.style['width'] = "100%"
element.appendChild(previewWidget.parentEl);
previewWidget.audioEl = document.createElement("audio");
previewWidget.audioEl.controls = true;
previewWidget.audioEl.loop = false;
previewWidget.audioEl.muted = false;
previewWidget.audioEl.style['width'] = "100%"
previewWidget.audioEl.addEventListener("loadedmetadata", () => {
previewWidget.aspectRatio = previewWidget.audioEl.audioWidth / previewWidget.audioEl.audioHeight;
fitHeight(node);
});
previewWidget.audioEl.addEventListener("error", () => {
//TODO: consider a way to properly notify the user why a preview isn't shown.
previewWidget.parentEl.hidden = true;
fitHeight(node);
});
let params = {
"filename": file,
"type": "input",
}
previewWidget.parentEl.hidden = previewWidget.value.hidden;
// previewWidget.audioEl.autoplay = !previewWidget.value.paused && !previewWidget.value.hidden;
let target_width = 256
if (element.style?.width) {
//overscale to allow scrolling. Endpoint won't return higher than native
target_width = element.style.width.slice(0,-2)*2;
}
if (!params.force_size || params.force_size.includes("?") || params.force_size == "Disabled") {
params.force_size = target_width+"x?"
} else {
let size = params.force_size.split("x")
let ar = parseInt(size[0])/parseInt(size[1])
params.force_size = target_width+"x"+(target_width/ar)
}
previewWidget.audioEl.src = api.apiURL('/view?' + new URLSearchParams(params));
previewWidget.audioEl.hidden = false;
previewWidget.parentEl.appendChild(previewWidget.audioEl)
}
function audioUpload(node, inputName, inputData, app) {
const audioWidget = node.widgets.find((w) => w.name === "audio");
let uploadWidget;
/*
A method that returns the required style for the html
*/
var default_value = audioWidget.value;
Object.defineProperty(audioWidget, "value", {
set : function(value) {
this._real_value = value;
},
get : function() {
let value = "";
if (this._real_value) {
value = this._real_value;
} else {
return default_value;
}
if (value.filename) {
let real_value = value;
value = "";
if (real_value.subfolder) {
value = real_value.subfolder + "/";
}
value += real_value.filename;
if(real_value.type && real_value.type !== "input")
value += ` [${real_value.type}]`;
}
return value;
}
});
async function uploadFile(file, updateNode, pasted = false) {
try {
// Wrap file in formdata so it includes filename
const body = new FormData();
body.append("image", file);
if (pasted) body.append("subfolder", "pasted");
const resp = await api.fetchApi("/upload/image", {
method: "POST",
body,
});
if (resp.status === 200) {
const data = await resp.json();
// Add the file to the dropdown list and update the widget value
let path = data.name;
if (data.subfolder) path = data.subfolder + "/" + path;
if (!audioWidget.options.values.includes(path)) {
audioWidget.options.values.push(path);
}
if (updateNode) {
audioWidget.value = path;
previewAudio(node,path)
}
} else {
alert(resp.status + " - " + resp.statusText);
}
} catch (error) {
alert(error);
}
}
const fileInput = document.createElement("input");
Object.assign(fileInput, {
type: "file",
accept: "audio/mp3,audio/wav,audio/flac,audio/m4a",
style: "display: none",
onchange: async () => {
if (fileInput.files.length) {
await uploadFile(fileInput.files[0], true);
}
},
});
document.body.append(fileInput);
// Create the button widget for selecting the files
uploadWidget = node.addWidget("button", "choose audio file to upload", "Audio", () => {
fileInput.click();
});
uploadWidget.serialize = false;
previewAudio(node, audioWidget.value);
const cb = node.callback;
audioWidget.callback = function () {
previewAudio(node,audioWidget.value);
if (cb) {
return cb.apply(this, arguments);
}
};
return { widget: uploadWidget };
}
ComfyWidgets.AUDIOPLOAD = audioUpload;
app.registerExtension({
name: "RVC-Studio.LoadAudio",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData?.name == "LoadAudio" && nodeData.category?.includes("RVC")) {
console.log({nodeData})
nodeData.input.required.upload = ["AUDIOPLOAD"];
}
},
});