From 97f5e8b2b79abdd705b88ee0731914cfdcbe66ed Mon Sep 17 00:00:00 2001 From: SayanoAI <28544690+SayanoAI@users.noreply.github.com> Date: Mon, 27 May 2024 19:40:15 -0400 Subject: [PATCH] added preview widgets --- custom_nodes/loaders.py | 5 +- custom_nodes/output.py | 17 +- custom_nodes/rvc.py | 11 +- custom_nodes/utils.py | 18 +- custom_nodes/uvr.py | 2 +- examples/complete-ai-voice-changer.json | 894 ++++++++++++++++++++++++ web/js/previewAudio.js | 156 ----- web/js/ui_handlers.js | 366 ++++++++++ web/js/uploadAudio.js | 203 ------ 9 files changed, 1301 insertions(+), 371 deletions(-) create mode 100644 examples/complete-ai-voice-changer.json delete mode 100644 web/js/previewAudio.js create mode 100644 web/js/ui_handlers.js delete mode 100644 web/js/uploadAudio.js diff --git a/custom_nodes/loaders.py b/custom_nodes/loaders.py index c44d3df..70df6d8 100644 --- a/custom_nodes/loaders.py +++ b/custom_nodes/loaders.py @@ -241,11 +241,12 @@ class LoadAudio: def load_audio(self, audio, sr): audio_path = folder_paths.get_annotated_filepath(audio) - print(f"{audio_path=} {audio=}") + widgetId = get_hash(audio_path) audio_name = os.path.basename(audio).split(".")[0] sr = None if sr=="None" else int(sr) audio = load_input_audio(audio_path,sr=sr) - return (audio_name,lambda:audio_to_bytes(*audio)) + return {"ui": {"preview": [{"filename": audio_name, "type": "input", "widgetId": widgetId}]}, "result": (audio_name, lambda:audio_to_bytes(*audio))} + # return (audio_name,lambda:audio_to_bytes(*audio)) @classmethod def IS_CHANGED(cls, audio): diff --git a/custom_nodes/output.py b/custom_nodes/output.py index 8a1ae4e..c4cde0a 100644 --- a/custom_nodes/output.py +++ b/custom_nodes/output.py @@ -1,10 +1,13 @@ import os +import shutil from ..lib.audio import SUPPORTED_AUDIO, audio_to_bytes, bytes_to_audio, load_input_audio, save_input_audio from ..lib.utils import get_hash from .utils import increment_filename_no_overwrite import folder_paths +temp_path = folder_paths.get_temp_directory() + class PreviewAudio: def __init__(self): pass @@ -18,6 +21,7 @@ class PreviewAudio: "save_format": (SUPPORTED_AUDIO,{"default": "flac"},), "save_channels": ([1,2],{"default": 1}), "overwrite_existing": ("BOOLEAN", {"default": True}), + "autoplay": ("BOOLEAN", {"default": True}), }, } @@ -30,7 +34,7 @@ class PreviewAudio: FUNCTION = "save_audio" - def save_audio(self, audio, filename, save_format, save_channels, overwrite_existing): + def save_audio(self, audio, filename, save_format, save_channels, overwrite_existing, autoplay): filename = filename.strip() assert filename, "Filename cannot be empty" @@ -44,10 +48,19 @@ class PreviewAudio: if os.path.isfile(output_path) and not overwrite_existing: output_path = increment_filename_no_overwrite(output_path) + input_audio = bytes_to_audio(audio()) print(save_input_audio(output_path,input_audio,to_int16=True,to_stereo=save_channels==2)) - return output_path, lambda:audio_to_bytes(*input_audio) + tempdir = os.path.join(temp_path,"preview") + os.makedirs(tempdir, exist_ok=True) + widgetId = get_hash(output_path,save_channels) + audio_name = f"{widgetId}.{save_format}" + preview_file = os.path.join(tempdir,audio_name) + if not os.path.isfile(preview_file): shutil.copyfile(output_path,preview_file) + return {"ui": {"preview": [{ + "filename": audio_name, "type": "temp", "subfolder": "preview", "widgetId": widgetId, "autoplay": autoplay + }]}, "result": (output_path, lambda:audio_to_bytes(*input_audio))} @classmethod def IS_CHANGED(cls, audio): diff --git a/custom_nodes/rvc.py b/custom_nodes/rvc.py index 3483fa4..341097b 100644 --- a/custom_nodes/rvc.py +++ b/custom_nodes/rvc.py @@ -1,4 +1,5 @@ import os +import shutil from ..lib.audio import audio_to_bytes, bytes_to_audio, load_input_audio, save_input_audio @@ -57,7 +58,8 @@ class RVCNode: def convert(self, audio, model, hubert_model, pitch_extraction_params, format, use_cache=True, **kwargs): - cache_name = os.path.join(BASE_CACHE_DIR,"rvc",f"{get_hash(audio(), model, *pitch_extraction_params.items())}.{format}") + widgetId = get_hash(audio(), model, *pitch_extraction_params.items()) + cache_name = os.path.join(BASE_CACHE_DIR,"rvc",f"{widgetId}.{format}") if use_cache and os.path.isfile(cache_name): output_audio = load_input_audio(cache_name) else: @@ -65,7 +67,12 @@ class RVCNode: output_audio = vc_single(hubert_model=hubert_model(),input_audio=input_audio,**model(),**pitch_extraction_params,**kwargs) print(save_input_audio(cache_name, output_audio)) - return (lambda:audio_to_bytes(*output_audio),) + tempdir = os.path.join(temp_path,"preview") + os.makedirs(tempdir, exist_ok=True) + audio_name = os.path.basename(cache_name) + preview_file = os.path.join(tempdir,audio_name) + if not os.path.isfile(preview_file): shutil.copyfile(cache_name,preview_file) + return {"ui": {"preview": [{"filename": audio_name, "type": "temp", "subfolder": "preview", "widgetId": widgetId}]}, "result": (lambda:audio_to_bytes(*output_audio),)} @classmethod diff --git a/custom_nodes/utils.py b/custom_nodes/utils.py index 0deb3b5..ee40c9a 100644 --- a/custom_nodes/utils.py +++ b/custom_nodes/utils.py @@ -10,7 +10,7 @@ from ..lib.utils import gc_collect, get_hash from .settings.downloader import RVC_DOWNLOAD_LINK, download_file from ..lib import BASE_MODELS_DIR import numpy as np -from ..lib.audio import MAX_INT16, audio_to_bytes, bytes_to_audio, merge_audio, remix_audio +from ..lib.audio import MAX_INT16, audio_to_bytes, bytes_to_audio, load_input_audio, merge_audio, remix_audio, save_input_audio CATEGORY = "🌺RVC-Studio/utils" temp_path = folder_paths.get_temp_directory() @@ -357,10 +357,18 @@ class MergeAudioNode: def merge(self, audio1, audio2, sr, merge_type, normalize): if sr=="None": sr=None - input_audio1 = bytes_to_audio(audio1()) - input_audio2 = bytes_to_audio(audio2()) - merged_audio = merge_audio(input_audio1,input_audio2,sr=sr,to_int16=True,norm=normalize,merge_type=merge_type) - return (lambda: audio_to_bytes(*merged_audio),) + + widgetId = get_hash(audio1(),audio2(),sr,merge_type,normalize) + audio_path = os.path.join(temp_path,"preview",f"{widgetId}.flac") + + if os.path.isfile(audio_path): merged_audio = load_input_audio(audio_path, sr, norm=normalize) + else: + input_audio1 = bytes_to_audio(audio1()) + input_audio2 = bytes_to_audio(audio2()) + merged_audio = merge_audio(input_audio1,input_audio2,sr=sr,to_int16=True,norm=normalize,merge_type=merge_type) + print(save_input_audio(audio_path,merged_audio)) + audio_name = os.path.basename(audio_path) + return {"ui": {"preview": [{"filename": audio_name, "type": "temp", "subfolder": "preview", "widgetId": widgetId}]}, "result": (lambda: audio_to_bytes(*merged_audio),)} @classmethod def IS_CHANGED(cls, *args, **kwargs): diff --git a/custom_nodes/uvr.py b/custom_nodes/uvr.py index fd36b3b..67b4d3b 100644 --- a/custom_nodes/uvr.py +++ b/custom_nodes/uvr.py @@ -84,4 +84,4 @@ class UVR5Node: @classmethod def IS_CHANGED(cls, *args, **kwargs): print(f"{args=} {kwargs=}") - return get_hash(*args, *kwargs.items()) \ No newline at end of file + return get_hash(args,kwargs) \ No newline at end of file diff --git a/examples/complete-ai-voice-changer.json b/examples/complete-ai-voice-changer.json new file mode 100644 index 0000000..e976cee --- /dev/null +++ b/examples/complete-ai-voice-changer.json @@ -0,0 +1,894 @@ +{ + "last_node_id": 39, + "last_link_id": 64, + "nodes": [ + { + "id": 7, + "type": "UVR5Node", + "pos": [ + 269, + -736 + ], + "size": { + "0": 315, + "1": 170 + }, + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "audio", + "type": "VHS_AUDIO", + "link": 20 + } + ], + "outputs": [ + { + "name": "primary_stem", + "type": "VHS_AUDIO", + "links": [ + 49 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "secondary_stem", + "type": "VHS_AUDIO", + "links": [ + 51 + ], + "shape": 3, + "slot_index": 1 + }, + { + "name": "audio_passthrough", + "type": "VHS_AUDIO", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "UVR5Node" + }, + "widgets_values": [ + "karafan/MDX23C-8KFFT-InstVoc_HQ.ckpt", + 10, + "flac", + true + ] + }, + { + "id": 14, + "type": "UVR5Node", + "pos": [ + 277, + -505 + ], + "size": { + "0": 315, + "1": 170 + }, + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "audio", + "type": "VHS_AUDIO", + "link": 24 + } + ], + "outputs": [ + { + "name": "primary_stem", + "type": "VHS_AUDIO", + "links": [ + 50 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "secondary_stem", + "type": "VHS_AUDIO", + "links": [ + 52 + ], + "shape": 3, + "slot_index": 1 + }, + { + "name": "audio_passthrough", + "type": "VHS_AUDIO", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "UVR5Node" + }, + "widgets_values": [ + "UVR/HP5-vocals+instrumentals.pth", + 10, + "flac", + true + ] + }, + { + "id": 17, + "type": "UVR5Node", + "pos": [ + 272.40658221699283, + -161.99581170557863 + ], + "size": { + "0": 315, + "1": 170 + }, + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "name": "audio", + "type": "VHS_AUDIO", + "link": 53 + } + ], + "outputs": [ + { + "name": "primary_stem", + "type": "VHS_AUDIO", + "links": [ + 56 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "secondary_stem", + "type": "VHS_AUDIO", + "links": [], + "shape": 3, + "slot_index": 1 + }, + { + "name": "audio_passthrough", + "type": "VHS_AUDIO", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "UVR5Node" + }, + "widgets_values": [ + "UVR/5_HP-Karaoke-UVR.pth", + 10, + "flac", + true + ] + }, + { + "id": 5, + "type": "LoadHubertModel", + "pos": [ + 306, + 51 + ], + "size": { + "0": 315, + "1": 58 + }, + "flags": { + "collapsed": true + }, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "hubert_model", + "type": "HUBERT_MODEL", + "links": [ + 58 + ], + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "LoadHubertModel" + }, + "widgets_values": [ + "hubert_base.pt" + ] + }, + { + "id": 6, + "type": "LoadPitchExtractionParams", + "pos": [ + 288, + 105 + ], + "size": { + "0": 315, + "1": 178 + }, + "flags": {}, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "pitch_extraction_params", + "type": "PITCH_EXTRACTION", + "links": [ + 59 + ], + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "LoadPitchExtractionParams" + }, + "widgets_values": [ + "rmvpe+", + false, + 0.75, + 0, + 0.25, + 0.25 + ] + }, + { + "id": 4, + "type": "LoadRVCModelNode", + "pos": [ + 649, + -136 + ], + "size": { + "0": 315, + "1": 78 + }, + "flags": {}, + "order": 2, + "mode": 0, + "outputs": [ + { + "name": "model", + "type": "RVC_MODEL", + "links": [ + 57 + ], + "shape": 3 + }, + { + "name": "model_name", + "type": "STRING", + "links": [ + 40 + ], + "shape": 3, + "slot_index": 1 + } + ], + "properties": { + "Node name for S&R": "LoadRVCModelNode" + }, + "widgets_values": [ + "RVC/loss27_Sayano_e151.pth" + ] + }, + { + "id": 35, + "type": "MergeAudioNode", + "pos": [ + 666, + -728 + ], + "size": [ + 315, + 126 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "audio1", + "type": "VHS_AUDIO", + "link": 49 + }, + { + "name": "audio2", + "type": "VHS_AUDIO", + "link": 50 + } + ], + "outputs": [ + { + "name": "vhs_audio", + "type": "VHS_AUDIO", + "links": [ + 53 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "MergeAudioNode" + }, + "widgets_values": [ + "None", + "min", + true, + { + "hidden": false, + "paused": false, + "params": {} + } + ] + }, + { + "id": 36, + "type": "MergeAudioNode", + "pos": [ + 676, + -528 + ], + "size": { + "0": 315, + "1": 126 + }, + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "name": "audio1", + "type": "VHS_AUDIO", + "link": 51 + }, + { + "name": "audio2", + "type": "VHS_AUDIO", + "link": 52 + } + ], + "outputs": [ + { + "name": "vhs_audio", + "type": "VHS_AUDIO", + "links": [ + 54 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "MergeAudioNode" + }, + "widgets_values": [ + "None", + "mean", + true + ] + }, + { + "id": 37, + "type": "RVCNode", + "pos": [ + 643, + 5 + ], + "size": { + "0": 309.34271240234375, + "1": 166 + }, + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "name": "audio", + "type": "VHS_AUDIO", + "link": 56 + }, + { + "name": "model", + "type": "RVC_MODEL", + "link": 57 + }, + { + "name": "hubert_model", + "type": "HUBERT_MODEL", + "link": 58 + }, + { + "name": "pitch_extraction_params", + "type": "PITCH_EXTRACTION", + "link": 59 + } + ], + "outputs": [ + { + "name": "VHS_AUDIO", + "type": "VHS_AUDIO", + "links": [ + 60 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "RVCNode" + }, + "widgets_values": [ + 0, + "flac", + true + ] + }, + { + "id": 33, + "type": "MergeAudioNode", + "pos": [ + 1154, + -367 + ], + "size": { + "0": 315, + "1": 126 + }, + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "name": "audio1", + "type": "VHS_AUDIO", + "link": 54 + }, + { + "name": "audio2", + "type": "VHS_AUDIO", + "link": 60 + } + ], + "outputs": [ + { + "name": "vhs_audio", + "type": "VHS_AUDIO", + "links": [ + 63 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "MergeAudioNode" + }, + "widgets_values": [ + "None", + "mean", + true + ] + }, + { + "id": 31, + "type": "JoinStrings", + "pos": [ + 726, + 241 + ], + "size": { + "0": 315, + "1": 106 + }, + "flags": { + "collapsed": true + }, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "string1", + "type": "STRING", + "link": 62, + "widget": { + "name": "string1" + } + }, + { + "name": "string2", + "type": "STRING", + "link": 40, + "widget": { + "name": "string2" + } + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 64 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "JoinStrings" + }, + "widgets_values": [ + "", + "", + "\\" + ] + }, + { + "id": 39, + "type": "PreviewAudio", + "pos": [ + 1514, + -358 + ], + "size": { + "0": 315, + "1": 150 + }, + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "name": "audio", + "type": "VHS_AUDIO", + "link": 63 + }, + { + "name": "filename", + "type": "STRING", + "link": 64, + "widget": { + "name": "filename" + }, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "output_path", + "type": "STRING", + "links": null, + "shape": 3 + }, + { + "name": "vhs_audio", + "type": "VHS_AUDIO", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "PreviewAudio" + }, + "widgets_values": [ + "test", + "flac", + 2, + true, + true + ] + }, + { + "id": 38, + "type": "LoadAudio", + "pos": [ + -171, + -739 + ], + "size": [ + 309.89001187135455, + 126 + ], + "flags": {}, + "order": 3, + "mode": 0, + "outputs": [ + { + "name": "audio_name", + "type": "STRING", + "links": [ + 62 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "vhs_audio", + "type": "VHS_AUDIO", + "links": [ + 61 + ], + "shape": 3, + "slot_index": 1 + } + ], + "properties": { + "Node name for S&R": "LoadAudio" + }, + "widgets_values": [ + "already-enough.mp3", + "None", + "image" + ] + }, + { + "id": 11, + "type": "UVR5Node", + "pos": [ + -172, + -567 + ], + "size": { + "0": 315, + "1": 170 + }, + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "audio", + "type": "VHS_AUDIO", + "link": 61 + } + ], + "outputs": [ + { + "name": "primary_stem", + "type": "VHS_AUDIO", + "links": null, + "shape": 3, + "slot_index": 0 + }, + { + "name": "secondary_stem", + "type": "VHS_AUDIO", + "links": [ + 20, + 24 + ], + "shape": 3, + "slot_index": 1 + }, + { + "name": "audio_passthrough", + "type": "VHS_AUDIO", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "UVR5Node" + }, + "widgets_values": [ + "UVR/UVR-DeEcho-DeReverb.pth", + 10, + "flac", + true + ] + } + ], + "links": [ + [ + 20, + 11, + 1, + 7, + 0, + "VHS_AUDIO" + ], + [ + 24, + 11, + 1, + 14, + 0, + "VHS_AUDIO" + ], + [ + 40, + 4, + 1, + 31, + 1, + "STRING" + ], + [ + 49, + 7, + 0, + 35, + 0, + "VHS_AUDIO" + ], + [ + 50, + 14, + 0, + 35, + 1, + "VHS_AUDIO" + ], + [ + 51, + 7, + 1, + 36, + 0, + "VHS_AUDIO" + ], + [ + 52, + 14, + 1, + 36, + 1, + "VHS_AUDIO" + ], + [ + 53, + 35, + 0, + 17, + 0, + "VHS_AUDIO" + ], + [ + 54, + 36, + 0, + 33, + 0, + "VHS_AUDIO" + ], + [ + 56, + 17, + 0, + 37, + 0, + "VHS_AUDIO" + ], + [ + 57, + 4, + 0, + 37, + 1, + "RVC_MODEL" + ], + [ + 58, + 5, + 0, + 37, + 2, + "HUBERT_MODEL" + ], + [ + 59, + 6, + 0, + 37, + 3, + "PITCH_EXTRACTION" + ], + [ + 60, + 37, + 0, + 33, + 1, + "VHS_AUDIO" + ], + [ + 61, + 38, + 1, + 11, + 0, + "VHS_AUDIO" + ], + [ + 62, + 38, + 0, + 31, + 0, + "STRING" + ], + [ + 63, + 33, + 0, + 39, + 0, + "VHS_AUDIO" + ], + [ + 64, + 31, + 0, + 39, + 1, + "STRING" + ] + ], + "groups": [ + { + "title": "Preprocess", + "bounding": [ + -181, + -813, + 336, + 507 + ], + "color": "#3f789e", + "font_size": 24 + }, + { + "title": "Ensemble Vocal", + "bounding": [ + 259, + -810, + 742, + 485 + ], + "color": "#3f789e", + "font_size": 24 + }, + { + "title": "Postprocess", + "bounding": [ + 262, + -236, + 712, + 529 + ], + "color": "#3f789e", + "font_size": 24 + }, + { + "title": "Output Audio", + "bounding": [ + 1144, + -441, + 698, + 255 + ], + "color": "#3f789e", + "font_size": 24 + } + ], + "config": {}, + "extra": { + "ds": { + "scale": 0.5644739300537777, + "offset": [ + -568.7175814310663, + 890.3856832248159 + ] + } + }, + "version": 0.4 +} \ No newline at end of file diff --git a/web/js/previewAudio.js b/web/js/previewAudio.js deleted file mode 100644 index 2a654e2..0000000 --- a/web/js/previewAudio.js +++ /dev/null @@ -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) - } - } -}); \ No newline at end of file diff --git a/web/js/ui_handlers.js b/web/js/ui_handlers.js new file mode 100644 index 0000000..8d73729 --- /dev/null +++ b/web/js/ui_handlers.js @@ -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 + } + } + }, +}); + diff --git a/web/js/uploadAudio.js b/web/js/uploadAudio.js deleted file mode 100644 index 5224316..0000000 --- a/web/js/uploadAudio.js +++ /dev/null @@ -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"]; - } - }, -}); -