initial refactor of rvc studio

This commit is contained in:
SayanoAI
2024-05-13 01:42:24 -04:00
parent 2d8f191d61
commit aa3493d87c
139 changed files with 25883 additions and 1 deletions
View File
+254
View File
@@ -0,0 +1,254 @@
import os
import subprocess
import sys
from .settings import PITCH_EXTRACTION_OPTIONS
from ..lib import BASE_MODELS_DIR
from ..lib.model_utils import load_hubert
from ..lib.utils import get_file_hash, get_filenames, get_hash, get_optimal_torch_device
from .utils import model_downloader
import torch
import folder_paths
from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor, pipeline
from ..vc_infer_pipeline import get_vc
from .settings.downloader import RVC_DOWNLOAD_LINK, RVC_MODELS, download_file
from ..lib.audio import SUPPORTED_AUDIO, audio_to_bytes, load_input_audio
from ..config import config
input_path = folder_paths.get_input_directory()
temp_path = folder_paths.get_temp_directory()
model_ids = [
'openai/whisper-large-v3',
'openai/whisper-large-v2',
'openai/whisper-large',
'openai/whisper-medium',
'openai/whisper-small',
'openai/whisper-base',
'openai/whisper-tiny',
'openai/whisper-medium.en',
'openai/whisper-small.en',
'openai/whisper-base.en',
'openai/whisper-tiny.en',
]
languages = ['en', 'fr', 'es']
spacy_model_map = {
"en": "en_core_web_md",
"fr": "fr_core_news_md",
"es": "es_core_news_md"
}
class LoadPitchExtractionParams:
@classmethod
def INPUT_TYPES(cls):
return {
'required': {
'f0_method': (PITCH_EXTRACTION_OPTIONS,{"default": "rmvpe"}),
"f0_autotune": ("BOOLEAN",),
"index_rate": ("FLOAT",{
"default": .75,
"min": 0., #Minimum value
"max": 1., #Maximum value
"step": .01, #Slider's step
"display": "slider"
}),
"resample_sr": ([0,16000,32000,40000,44100,48000],{"default": 0}),
"rms_mix_rate": ("FLOAT",{
"default": 0.25,
"min": 0., #Minimum value
"max": 1., #Maximum value
"step": .01, #Slider's step
"display": "slider"
}),
"protect": ("FLOAT",{
"default": 0.25,
"min": 0., #Minimum value
"max": .5, #Maximum value
"step": .01, #Slider's step
"display": "slider"
})
},
}
RETURN_TYPES = ('PITCH_EXTRACTION', )
RETURN_NAMES = ('pitch_extraction_params', )
CATEGORY = "🌺RVC-Studio/loaders"
FUNCTION = 'load_params'
def load_params(self, **params):
if "rmvpe" in params.get("f0_method",""): model_downloader("rmvpe.pt")
return (params,)
class LoadHubertModel:
@classmethod
def INPUT_TYPES(cls):
model_list = ["hubert_base.pt"] + get_filenames(root=BASE_MODELS_DIR,folder="*",exts=["pt"],format_func=os.path.basename)
return {
'required': {
'model': (model_list,{"default": "hubert_base.pt"}),
},
}
RETURN_TYPES = ('HUBERT_MODEL', )
RETURN_NAMES = ('hubert_model', )
CATEGORY = "🌺RVC-Studio/loaders"
FUNCTION = 'load_model'
def load_model(self, model):
model_path = model_downloader(model)
hubert_model = lambda:load_hubert(model_path,config=config)
return (hubert_model,)
@classmethod
def IS_CHANGED(cls, model):
return get_hash(model)
class LoadRVCModelNode:
@classmethod
def INPUT_TYPES(cls):
model_list = RVC_MODELS + get_filenames(root=BASE_MODELS_DIR,folder="RVC",exts=["pth"],format_func=lambda x: f"RVC/{os.path.basename(x)}")
model_list = list(set(model_list)) # dedupe
return {
'required': {
'model': (model_list,{"default": "RVC/Sayano.pth"}),
},
}
RETURN_TYPES = ('RVC_MODEL', 'STRING')
RETURN_NAMES = ('model', 'model_name')
CATEGORY = "🌺RVC-Studio/loaders"
FUNCTION = 'load_model'
@classmethod
def IS_CHANGED(cls, model):
return get_hash(model)
def load_model(self, model):
try:
filename = os.path.basename(model)
subfolder = os.path.dirname(model)
model_path = os.path.join(BASE_MODELS_DIR,subfolder,filename)
if not os.path.isfile(model_path):
download_link = f"{RVC_DOWNLOAD_LINK}{model}"
params = model_path, download_link
if download_file(params): print(f"successfully downloaded: {model_path}")
model = lambda:get_vc(model_path)
return (model,filename.split(".")[0])
except Exception as e:
print(f"Error in {self.__class__.__name__}: {e}")
raise e
class LoadWhisperModelNode:
@classmethod
def INPUT_TYPES(cls):
return {
'required': {
'model_id': (model_ids,{"default": "openai/whisper-base.en"}),
},
"optional": {
'language': (languages,{"default": "en"}),
"max_new_tokens": ("INT", {"default": 128, "min": 16, "max": 1024, "display": "slider"}),
"chunk_length_s": ("INT", {"default": 30, "min": 15, "max": 60, "display": "slider"}),
"batch_size": ("INT", {"default": 16, "min": 1, "max": 128, "display": "slider"}),
}
}
RETURN_TYPES = ('TRANSCRIPTION_MODEL', )
RETURN_NAMES = ('model', )
CATEGORY = "🌺RVC-Studio/loaders"
FUNCTION = 'load_model'
@classmethod
def IS_CHANGED(cls, model):
return get_hash(model)
def load_model(self, model_id, language="en", max_new_tokens=128, chunk_length_s=12, batch_size=16):
device = get_optimal_torch_device()
torch_dtype = torch.float16 if torch.cuda.is_available() else torch.float32
model = AutoModelForSpeechSeq2Seq.from_pretrained(
model_id, torch_dtype=torch_dtype, low_cpu_mem_usage=True, use_safetensors=True
)
processor = AutoProcessor.from_pretrained(model_id)
model.to(device)
generate_kwargs = {}
if model_id.endswith('.en'):
if language != 'en':
raise ValueError(f'Model {model_id} only supports English language')
else:
generate_kwargs['language'] = language
def pipe():
def get_spacy_model():
import spacy
model_name = spacy_model_map[language]
model_path = os.path.join(BASE_MODELS_DIR,model_name)
if not os.path.exists(model_path):
subprocess.call([sys.executable, "-m", "spacy", "download", model_name])
model = spacy.load(model_name)
model.to_disk(model_path)
return spacy.load(model_path)
return pipeline(
'automatic-speech-recognition',
model=model,
tokenizer=processor.tokenizer,
feature_extractor=processor.feature_extractor,
max_new_tokens=max_new_tokens,
chunk_length_s=chunk_length_s,
batch_size=batch_size,
return_timestamps=True,
torch_dtype=torch_dtype,
device=device,
generate_kwargs=generate_kwargs,
),get_spacy_model
return (pipe, )
class LoadAudio:
@classmethod
def INPUT_TYPES(cls):
input_dir = input_path
files = get_filenames(root=input_dir,exts=SUPPORTED_AUDIO,format_func=os.path.basename)
return {
"required": {
"audio": (files,),
"sr": (["None",16000,44100,48000],{"default": "None"}),
}}
CATEGORY = "🌺RVC-Studio/loaders"
RETURN_TYPES = ("STRING","VHS_AUDIO")
RETURN_NAMES = ("audio_name","vhs_audio")
FUNCTION = "load_audio"
def load_audio(self, audio, sr):
audio_path = folder_paths.get_annotated_filepath(audio)
print(f"{audio_path=} {audio=}")
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))
@classmethod
def IS_CHANGED(cls, audio):
audio_path = folder_paths.get_annotated_filepath(audio)
print(f"{audio_path=}")
return get_file_hash(audio_path)
+54
View File
@@ -0,0 +1,54 @@
import os
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
class PreviewAudio:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"audio": ("VHS_AUDIO",),
"filename": ("STRING",{"default": "test"}),
"save_format": (SUPPORTED_AUDIO,{"default": "flac"},),
"save_channels": ([1,2],{"default": 1}),
"overwrite_existing": ("BOOLEAN", {"default": True}),
},
}
RETURN_TYPES = ("STRING","VHS_AUDIO")
RETURN_NAMES = ("output_path","vhs_audio")
OUTPUT_NODE = True
CATEGORY = "🌺RVC-Studio/output"
FUNCTION = "save_audio"
def save_audio(self, audio, filename, save_format, save_channels, overwrite_existing):
filename = filename.strip()
assert filename, "Filename cannot be empty"
base_output_dir = folder_paths.get_output_directory()
assert os.path.exists(base_output_dir), f"Output directory {base_output_dir} does not exist"
output_dir = os.path.join(base_output_dir, 'audio')
os.makedirs(output_dir, exist_ok=True)
output_path = os.path.join(output_dir, f"{filename}.{save_format}")
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)
@classmethod
def IS_CHANGED(cls, audio):
return get_hash(audio())
+76
View File
@@ -0,0 +1,76 @@
import os
from ..lib.audio import audio_to_bytes, bytes_to_audio, load_input_audio, save_input_audio
from ..vc_infer_pipeline import vc_single
import folder_paths
from ..lib.utils import get_hash, get_optimal_torch_device
from ..lib import BASE_CACHE_DIR, BASE_MODELS_DIR
input_path = folder_paths.get_input_directory()
temp_path = folder_paths.get_temp_directory()
# output_path = folder_paths.get_output_directory()
# base_path = os.path.dirname(input_path)
node_path = os.path.join(BASE_MODELS_DIR,"custom_nodes/ComfyUI-UVR5")
weights_path = os.path.join(BASE_MODELS_DIR, "uvr5")
device = get_optimal_torch_device()
is_half = True
class RVCNode:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"audio": ("VHS_AUDIO",),
"model": ("RVC_MODEL",),
"hubert_model": ("HUBERT_MODEL",),
"pitch_extraction_params": ("PITCH_EXTRACTION",),
"f0_up_key": ("INT",{
"default": 0,
"min": -14, #Minimum value
"max": 14, #Maximum value
"step": 1, #Slider's step
"display": "slider"
}),
"format":(["wav", "flac", "mp3"],{
"default": "flac"
}),
},
"optional": {
"use_cache": ("BOOLEAN",{"default": True})
}
}
OUTPUT_NODE = True
RETURN_TYPES = ("VHS_AUDIO",)
FUNCTION = "convert"
CATEGORY = "🌺RVC-Studio/rvc"
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}")
if use_cache and os.path.isfile(cache_name): output_audio = load_input_audio(cache_name)
else:
input_audio = bytes_to_audio(audio())
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),)
@classmethod
def IS_CHANGED(cls, *args, **kwargs):
print(f"{args=} {kwargs=}")
return get_hash(*args, *kwargs.items())
+30
View File
@@ -0,0 +1,30 @@
from functools import lru_cache
import os
from ...lib import BASE_CACHE_DIR, PersistedDict
MENU_ITEMS = {
"Get help": "https://github.com/SayanoAI/RVC-Studio/discussions",
"Report a Bug": "https://github.com/SayanoAI/RVC-Studio/issues",
"About": """This project provides a comprehensive platform for training RVC models and generating AI voice covers.
Check out this github for more info: https://github.com/SayanoAI/RVC-Studio
"""
}
DEVICE_OPTIONS = ["cpu","cuda"]
PITCH_EXTRACTION_OPTIONS = ["rmvpe","rmvpe+"]
MERGE_OPTIONS=["median","mean","min","max"]
TTS_MODELS = ["edge","speecht5"]
N_THREADS_OPTIONS=[1,2,4,8,12,16]
SR_MAP = {"32k": 32000,"40k": 40000, "48k": 48000}
@lru_cache
def get_servers():
os.makedirs(BASE_CACHE_DIR,exist_ok=True)
fname = os.path.join(BASE_CACHE_DIR,"servers.shelve")
servers = PersistedDict(fname)
return servers
SERVERS = get_servers()
RVC_INFERENCE_URL = SERVERS.RVC_INFERENCE_URL
UVR_INFERENCE_URL = SERVERS.UVR_INFERENCE_URL
+121
View File
@@ -0,0 +1,121 @@
import re
from typing import IO, List, Tuple
import unicodedata
import requests
import os
import zipfile
from ...lib import BASE_CACHE_DIR, BASE_MODELS_DIR
RVC_DOWNLOAD_LINK = 'https://huggingface.co/datasets/SayanoAI/RVC-Studio/resolve/main/'
MDX_MODELS = ["MDXNET/UVR-MDX-NET-vocal_FT.onnx"]
KARAFAN_MODELS = ["karafan/MDX23C-8KFFT-InstVoc_HQ.ckpt"]
VR_MODELS = ["UVR/UVR-DeEcho-DeReverb.pth","UVR/HP5-vocals+instrumentals.pth"]
RVC_MODELS = [
"RVC/Claire.pth",
"RVC/Sayano.pth",
"RVC/Mae_v2.pth",
"RVC/Fuji.pth",
"RVC/Monika.pth"]
BASE_MODELS = ["hubert_base.pt", "rmvpe.pt"]
VITS_MODELS = ["VITS/pretrained_ljs.pth"]
PRETRAINED_MODELS = [
"pretrained_v2/D48k.pth",
"pretrained_v2/G48k.pth",
"pretrained_v2/D32k.pth",
"pretrained_v2/G32k.pth",
"pretrained_v2/D40k.pth",
"pretrained_v2/G40k.pth",
"pretrained_v2/f0D48k.pth",
"pretrained_v2/f0G48k.pth",
"pretrained_v2/f0D40k.pth",
"pretrained_v2/f0G40k.pth",
"pretrained_v2/f0D32k.pth",
"pretrained_v2/f0G32k.pth",
"pretrained_v2/f0_RIN_E3_40k_D.pth",
"pretrained_v2/f0_RIN_E3_40k_G.pth",
"pretrained_v2/f0Ov2Super32kD.pth",
"pretrained_v2/f0Ov2Super32kG.pth",
"pretrained_v2/f0Ov2Super40kD.pth",
"pretrained_v2/f0Ov2Super40kG.pth",
]
LLM_MODELS = [
"https://huggingface.co/TheBloke/Airoboros-L2-7B-2.1-GGUF/resolve/main/airoboros-l2-7b-2.1.Q4_K_M.gguf",
"https://huggingface.co/TheBloke/Pygmalion-2-7B-GGUF/resolve/main/pygmalion-2-7b.Q4_K_M.gguf",
"https://huggingface.co/TheBloke/Zarablend-MX-L2-7B-GGUF/resolve/main/zarablend-mx-l2-7b.Q4_K_M.gguf",
"https://huggingface.co/TheBloke/MythoMax-L2-Kimiko-v2-13B-GGUF/resolve/main/mythomax-l2-kimiko-v2-13b.Q4_K_M.gguf"
]
STT_MODELS = [
"https://alphacephei.com/vosk/models/vosk-model-en-us-0.22-lgraph.zip"
]
def download_file(params: Tuple[str, str]):
model_path, download_link = params
if os.path.isfile(model_path): raise FileExistsError(f"{model_path} already exists!")
os.makedirs(os.path.dirname(model_path), exist_ok=True)
with requests.get(download_link,stream=True) as r:
r.raise_for_status()
with open(model_path, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
return True
def download_link_generator(download_link: str,model_list: List[str]):
for model in model_list:
model_path = os.path.join(BASE_MODELS_DIR,model)
yield (model_path, f"{download_link}{model}")
def save_file(params: Tuple[str, any]):
(data_path, datum) = params
if "zip" in os.path.splitext(data_path)[-1]: return save_zipped_files(params) # unzip
else:
try:
with open(data_path,"wb") as f:
f.write(datum)
return f"Successfully saved file to: {data_path}"
except Exception as e:
return f"Failed to save file: {e}"
def save_file_generator(save_dir: str, data: List[IO]):
for datum in data:
data_path = os.path.join(save_dir,datum.name)
yield (data_path, datum.read())
def save_zipped_files(params: Tuple[str, any]):
(data_path, datum) = params
try:
print(f"saving zip file: {data_path}")
temp_dir = os.path.join(BASE_CACHE_DIR,"zips")
os.makedirs(temp_dir,exist_ok=True)
name = os.path.basename(data_path)
zip_path = os.path.join(temp_dir,name)
with open(zip_path,"wb") as f:
f.write(datum)
print(f"extracting zip file: {zip_path}")
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(os.path.dirname(data_path))
print(f"finished extracting zip file")
os.remove(zip_path) # cleanup
return f"Successfully saved files to: {data_path}"
except Exception as e:
return f"Failed to save files: {e}"
def slugify_filepath(filepath):
# Split the path into directory and filename
directory, filename = os.path.split(filepath)
# Normalize the filename
filename = unicodedata.normalize('NFKD', filename)
# Encode the filename as ASCII and ignore errors
filename = filename.encode('ascii', 'ignore').decode()
# Convert the filename to lowercase
filename = filename.lower()
# Replace spaces and other unwanted characters with dashes
filename = re.sub(r'[^a-z0-9.-]+', '-', filename)
# Join the directory and the slugified filename
return os.path.join(directory, filename)
+204
View File
@@ -0,0 +1,204 @@
import json
import os
from pprint import pprint
import numpy as np
from ..lib import BASE_CACHE_DIR
from .utils import increment_filename_no_overwrite
from ..lib.utils import get_hash
import folder_paths
from ..lib.audio import bytes_to_audio, remix_audio
import textacy
from textacy.extract import keyterms as kt
temp_path = folder_paths.get_temp_directory()
def extract_keywords(text: str, max_words: int, spacy_model, **kwargs):
text = text.strip()
doc = textacy.make_spacy_doc(text, lang=spacy_model)
topn = int(max_words) if max_words>0 else len(text)
include_pos = ["NOUN","ADJ","PROPN","VERB","NUM","ADP"]
ngrams = [1,2]
tags = []
try:
terms = kt.sgrank(doc, ngrams=ngrams, normalize="lower", topn=topn, include_pos=include_pos)
tags = list(map(lambda v:v[0],sorted(terms,key=lambda v:v[1],reverse=True)[:topn]))
except Exception as error:
print(f"{text=} {error=}")
return ", ".join(tags).strip()
def limit_sentence(text: str, max_words: int, **kwargs):
text = text.strip()
topn = int(max_words) if max_words>0 else len(text)
if topn>0: return " ".join(text.split()[:topn])
else: return text
class AudioTranscriptionNode:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"pipeline": ('TRANSCRIPTION_MODEL',),
"audio": ('VHS_AUDIO',),
"format_newlines_on_punctuation": ('BOOLEAN', {'default': True}),
"loop": ('BOOLEAN', {'default': True}),
"use_tags": ('BOOLEAN', {'default': True}),
"max_words": ('INT', {'default': 16, "min": 0, "max": 32, "display": "slider"}),
"frame_interpolation": ("INT", {"default": 0, "min": 0, "max": 120})
},
"optional": {
"save_transcription": ('BOOLEAN', {'default': False}),
"save_chunks": ('BOOLEAN', {'default': False}),
"save_filename": ('STRING', {'default': 'transcription'}),
"overwrite_existing": ('BOOLEAN', {'default': True}),
"print_output": ('BOOLEAN', {'default': True}),
"use_cache": ("BOOLEAN",{"default": True})
}
}
OUTPUT_NODE = True
RETURN_TYPES = ("STRING", "STRING", "STRING", "INT", "INT")
RETURN_NAMES = ("transcription", "batch_prompt_text", "batch_values_text", "num_chunks","num_frames")
FUNCTION = "transcribe"
CATEGORY = "🌺RVC-Studio/stt"
@staticmethod
def save_output(filename,text,overwrite_existing=False):
base_output_dir = folder_paths.get_output_directory()
assert os.path.exists(base_output_dir), f"Output directory {base_output_dir} does not exist"
output_dir = os.path.join(base_output_dir, 'transcriptions')
file_path = os.path.join(output_dir, filename)
dir_name = os.path.dirname(file_path)
os.makedirs(dir_name, exist_ok=True)
if os.path.exists(file_path) and not overwrite_existing:
file_path = increment_filename_no_overwrite(file_path)
with open(file_path, 'w') as f:
f.write(text)
@staticmethod
def process_text_chunks(ichunk,text_processor,frame_interpolation,**kwargs):
i,chunk=ichunk
frame_interpolation=int(frame_interpolation)
text = text_processor(chunk["text"],**kwargs)
timestamp = np.nan_to_num(np.array(chunk["timestamp"],dtype=float),nan=i)
index = int(timestamp[0]*frame_interpolation) if frame_interpolation>0 else i
prompt = f'"{index}":"{text}"' if len(text) else f'"{index}":""'
duration = max(abs(int(np.round(timestamp[1]-timestamp[0]))),1) # at least 1 sec duration
if frame_interpolation>1: duration*=frame_interpolation
value = f'"{index}":"({duration})"'
return prompt,value
@staticmethod
def load_cache(*args):
results = None
cache_name = os.path.join(BASE_CACHE_DIR,"stt",f"{get_hash(*args)}.json")
os.makedirs(os.path.dirname(cache_name), exist_ok=True)
if os.path.isfile(cache_name): results = json.load(cache_name)
return results, cache_name
def transcribe(
self, pipeline, audio,
format_newlines_on_punctuation,loop,
use_tags,max_words,frame_interpolation,
save_transcription=False, save_chunks=False,
save_filename="transcription", overwrite_existing=True,
print_output=True,use_cache=True
):
print('Starting Transcription')
audio_data = bytes_to_audio(audio())
if use_cache: result, cache_name = self.load_cache(pipeline,audio(),loop,use_tags,max_words,frame_interpolation)
else: result = None
audio_frames = int(np.ceil(len(audio_data[0])/audio_data[1]))
whisper_model,get_spacy_model=pipeline()
if result is None:
audio,_ = remix_audio(audio_data,target_sr=16000,norm=True)
result = whisper_model(audio)
if use_cache and result:
with open(cache_name, "w") as f:
json.dump(result, f)
print('Transcription Done')
text = result["text"]
total_chunks = result['chunks']
if format_newlines_on_punctuation:
punct = ['.', '?', '!']
for p in punct:
text = text.replace(f'{p} ', f'{p}\n')
if loop: # append first frame to chunk stack
last_chunk = None
for i in range(len(total_chunks)):
last_chunk = total_chunks[-1-i]
if "timestamp" in last_chunk and len(last_chunk["timestamp"])>=1:
last_chunk["timestamp"] = np.nan_to_num(np.array(last_chunk["timestamp"],dtype=float),nan=audio_frames)
break
if last_chunk is not None:
timestamp = last_chunk["timestamp"]
start_time = timestamp[-1]
duration = max(1,audio_frames - start_time)
total_chunks.append({
"text": total_chunks[0]["text"],
"timestamp": (start_time,start_time+duration)
})
else: # adds 1s frame to prevent audio from stopping early
num_frames+=1
if use_tags:
spacy_model,text_processor = get_spacy_model(),extract_keywords
else:
spacy_model,text_processor = None,limit_sentence
# split transcript into prompt based on timestamp
# find length of each frame using timestamp
prompts = []
values = []
for ichunk in enumerate(total_chunks):
p, v = self.process_text_chunks(ichunk,text_processor,frame_interpolation,spacy_model=spacy_model,max_words=max_words)
prompts.append(p)
values.append(v)
num_chunks = len(total_chunks)
batch_prompt = ',\n'.join(prompts)
batch_values = ',\n'.join(values)
num_frames = int(audio_frames*frame_interpolation if frame_interpolation>0 else audio_frames)
if print_output:
pprint(total_chunks)
print(batch_prompt)
print(batch_values)
print(f"{num_chunks=}, {audio_frames=}s, {num_frames=}")
if save_transcription:
self.save_output(save_filename + '.txt', text, overwrite_existing=overwrite_existing)
if save_chunks:
self.save_output(
save_filename + '_prompt.json',
json.dumps(json.loads("{"+batch_prompt+"}"),indent=2), # formats json with indent
overwrite_existing=overwrite_existing)
self.save_output(
save_filename + '_values.json',
json.dumps(json.loads("{"+batch_values+"}"),indent=2), # formats json with indent
overwrite_existing=overwrite_existing)
text = text_processor(text,max_words=max_words,spacy_model=spacy_model)
return (text, batch_prompt, batch_values, num_chunks, num_frames)
@classmethod
def IS_CHANGED(cls, *args, **kwargs):
print(f"{args=} {kwargs=}")
return get_hash(*args, *kwargs.items())
+368
View File
@@ -0,0 +1,368 @@
import itertools
import os
from pprint import pprint
import re
import tempfile
import torch
import folder_paths
from .settings import MERGE_OPTIONS
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
CATEGORY = "🌺RVC-Studio/utils"
temp_path = folder_paths.get_temp_directory()
def model_downloader(model):
filename = os.path.basename(model)
subfolder = os.path.dirname(model)
model_path = os.path.join(BASE_MODELS_DIR,subfolder,filename)
if not os.path.isfile(model_path):
download_link = f"{RVC_DOWNLOAD_LINK}{model}"
params = model_path, download_link
if download_file(params): print(f"successfully downloaded: {model_path}")
return model_path
def increment_filename_no_overwrite(proposed_path):
output_dir, filename_ext = os.path.split(proposed_path)
filename, file_format = os.splitext(filename_ext)
files = os.listdir(output_dir)
files = filter(lambda f: os.path.isfile(os.path.join(output_dir, f)), files)
files = filter(lambda f: f.startswith(filename), files)
files = filter(lambda f: f.endswith('.' + file_format), files)
file_numbers = [re.search(r'_(\d+)\.', f) for f in files]
file_numbers = [int(f.group(1)) for f in file_numbers if f]
this_file_number = max(file_numbers) + 1 if file_numbers else 1
output_path = os.path.join(output_dir, filename + f'_{this_file_number}.' + file_format)
return output_path
class MergeImageBatches:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
"iterate": ("BOOLEAN",{"default": False})
},
}
INPUT_IS_LIST = (True, False)
RETURN_TYPES = ("IMAGE",)
FUNCTION = "merge"
CATEGORY = CATEGORY
__archive__ = None
@staticmethod
def iter_images(fname,images):
size = len(images)
if size==0: return images
shape = images[0].shape
if len(shape)>3: shape=shape[1:]
memmap = np.memmap(fname, mode='w+', dtype=np.float32, shape=(size,*shape))
for i,img in enumerate(itertools.chain(*images)):
if i >= len(memmap): #need to expand memmap
print(f"index {i} is larger than {len(memmap)}. expanding memmap to {size+i}...")
memmap.flush()
memmap = np.memmap(fname, dtype=np.float32, mode='r+',shape=(size+i,*shape), order='F')
memmap[i]=img
# del img
# gc_collect()
memmap.flush()
print(f"{fname=} size={i} shape={memmap.shape}")
# del images
images = torch.from_numpy(memmap[:i])
return images
def __del__(self):
try:
if os.path.exists(self.__archive__): os.remove(self.__archive__)
gc_collect()
except Exception as error:
print(f"Failed to delete {self.__archive__}: {error=}")
def merge(self, images, iterate):
if len(images) <= 1:
return (images[0],)
else:
if hasattr(iterate,"pop"): iterate=iterate.pop()
print(f"{iterate=}")
if iterate:
if self.__archive__ is None:
with tempfile.NamedTemporaryFile(delete=False) as ntf:
self.__archive__ = ntf.name
images = self.iter_images(self.__archive__,images)
else:
images = torch.cat(images)
gc_collect()
print(f"Merged images: {len(images)=} {images.shape=}")
return (images,)
class MergeLatentBatches:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"latents": ("LATENT",),
}
}
INPUT_IS_LIST = True
RETURN_TYPES = ("LATENT",)
FUNCTION = "merge"
CATEGORY = CATEGORY
@staticmethod
def merge_tensor(tensors):
return torch.cat(tensors, dim=0)
def merge(self, latents):
merged_latents = {}
gc_collect()
merged_latents["samples"] = self.merge_tensor([latent["samples"] for latent in latents])
merged_latents["noise_mask"] = self.merge_tensor([latent["noise_mask"] for latent in latents])
merged_latents["batch_index"] = self.merge_tensor([latent["batch_index"] for latent in latents])
print(f'Merged latents: {len(merged_latents["samples"])=} {latents["batch_index"]=}')
return (merged_latents,)
class AudioBatchValueNode:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"audio": ('VHS_AUDIO',),
"num_frames": ('INT', {"min": 1, "max": 128, "step": 1}),
"output_min": ('FLOAT', {'default': 0., "min": -1000., "max": 1000., "step": .01}),
"output_max": ('FLOAT', {'default': 1., "min": 0., "max": 1000., "step": .01}),
"norm": (["scale","tanh","sigmoid"], {"default": "scale"}),
},
"optional": {
"silence_threshold": ("INT", {"default": 1000, "min": 1, "max": MAX_INT16, "step": 1, "display": "slider"}),
"frame_multiplier": ("INT", {"default": 1, "min": 1, "max": 120, "step": 1}),
"print_output": ("BOOLEAN", {"default": False}),
"inverse": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("STRING","INT")
RETURN_NAMES = ("batch_value_text","num_values")
FUNCTION = "get_frame_weights"
CATEGORY = CATEGORY
@staticmethod
def get_rms(audio): # root mean squared of audio segment
return np.sqrt(np.nanmean(audio**2))
def get_frame_weights(self, audio, num_frames, output_min, output_max, norm,
silence_threshold=1000, frame_multiplier=1, print_output=False, inverse=False):
assert output_max>=output_min, f"{output_max=} must be greater or equal to {output_min=}!"
audio_data = bytes_to_audio(audio())
audio,_ = remix_audio(audio_data,norm=True,to_int16=True)
num_values = int(num_frames*frame_multiplier)
audio_rms = np.nan_to_num(list(map(self.get_rms,np.array_split(audio.flatten()/silence_threshold, num_values))),nan=0)
audio_zscore = (audio_rms-audio_rms.mean())/audio_rms.std()
output_range = output_max-output_min
print(f"{audio_rms.min()=} {audio_rms.max()=} {audio_rms.mean()=}")
if norm=="tanh":
x_norm = np.tanh(audio_zscore) #tanh=-1 to 1
if inverse: x_norm*=-1
x_norm = (x_norm * output_range + output_max + output_min)/2
elif norm=="sigmoid":
x_norm = 1. / (1. + np.exp(-audio_zscore)) #sigmoid=0 to 1
if inverse: x_norm=1-x_norm
x_norm = x_norm * output_range + output_min
else:
x_min = audio_zscore.min()
x_norm = (audio_zscore - x_min) / (audio_zscore.max() - x_min) #scale=0 to 1
if inverse: x_norm=1-x_norm
x_norm = x_norm * output_range + output_min
print(f"{x_norm.min()=} {x_norm.max()=} {x_norm.mean()=}")
batch_value = ",\n".join([f'{n}:({v})' for n,v in enumerate(x_norm)])
if print_output: pprint(batch_value)
return (batch_value,num_values)
@classmethod
def IS_CHANGED(cls, *args, **kwargs):
print(f"{args=} {kwargs=}")
return get_hash(*args, *kwargs.items())
class ImageRepeatInterleavedNode:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
"repeats": ("INT", {"default": 1, "min": 1, "max": 4096}),
},
"optional": {
"fps": ("INT", {"default": 1, "min": 1})
}
}
RETURN_TYPES = ("IMAGE",)
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True, )
FUNCTION = "rebatch"
CATEGORY = CATEGORY
def rebatch(self, images, repeats, fps=1):
repeats = np.array(repeats).flatten()*fps
print(f"{repeats=}")
all_images = []
for img in images:
for i in range(img.shape[0]):
all_images.append(img[i:i+1])
# expand repeat
if len(repeats)==1:
repeats = repeats.repeat(len(all_images),axis=0)
elif len(repeats)<len(all_images):
pad_width = len(all_images) - len(repeats)
repeats = np.pad(repeats, (0, pad_width), mode='constant', constant_values=1)
for i,img in enumerate(all_images):
if repeats[i]>1: all_images[i] = img.expand(repeats[i], *img.shape).flatten(0,1)
else: all_images[i] = img
print(f"{img.shape=} {all_images[i].shape=}")
print(f"{len(images)=} {len(all_images)=}")
gc_collect()
return (all_images,)
class LatentRepeatInterleavedNode:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"latents": ("LATENT",),
"repeats": ("INT", {"default": 1, "min": 1, "max": 4096}),
},
"optional": {
"fps": ("INT", {"default": 1, "min": 1})
}
}
RETURN_TYPES = ("LATENT",)
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True, )
FUNCTION = "merge"
CATEGORY = CATEGORY
@staticmethod
def repeat(latents, repeat):
if latents.ndim>3: #batched
expanded_latents=(latents.expand(repeat, *latents.shape).flatten(0,1))
else:
expanded_latents=(latents.expand(repeat, *latents.shape))
print(f"{len(latents)}*{repeat}={len(expanded_latents)} ({latents.shape=} => {expanded_latents.shape=})")
return expanded_latents
@staticmethod
def rebatch(samples, repeats):
offset = 0
for i in range(len(samples)):
samples[i]["batch_index"] = [i+offset for i in range(repeats[i])]
offset += repeats[i]
samples[i]["samples"] = LatentRepeatInterleavedNode.repeat(samples[i]["samples"],repeats[i])
if "noise_mask" in samples[i]:
samples[i]["noise_mask"] = LatentRepeatInterleavedNode.repeat(samples[i]["noise_mask"],repeats[i])
return samples
def merge(self, latents, repeats, fps=1):
repeats = np.array(repeats).flatten()*fps
print(f"{repeats=}")
# combine all latents
new_latents = []
for latent in latents:
if latent["samples"].ndim==3: #unbatched
new_latents.append(latent)
else:
for i in range(len(latent["samples"])):
new_latent = {
"samples": latent["samples"][i],
}
if "noise_mask" in latent: new_latent["noise_mask"] = latent["noise_mask"][i]
new_latents.append(new_latent)
# expand repeat
if len(repeats)==1:
repeats = repeats.repeat(len(new_latents),axis=0)
elif len(repeats)<len(new_latents):
pad_width = len(new_latents) - len(repeats)
repeats = np.pad(repeats, (0, pad_width), mode='constant', constant_values=1)
new_latents = self.rebatch(new_latents,repeats.astype(int))
gc_collect()
return (new_latents,)
class MergeAudioNode:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"audio1": ("VHS_AUDIO",),
"audio2": ("VHS_AUDIO",),
"sr": (["None",32000,40000,44100,48000],{
"default": "None"
}),
"merge_type": (MERGE_OPTIONS,{"default": "mean"}),
"normalize": ("BOOLEAN",{"default": True})
}
}
RETURN_TYPES = ("VHS_AUDIO",)
RETURN_NAMES = ("vhs_audio",)
FUNCTION = "merge"
CATEGORY = CATEGORY
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),)
@classmethod
def IS_CHANGED(cls, *args, **kwargs):
print(f"{args=} {kwargs=}")
return get_hash(*args, *kwargs.items())
+86
View File
@@ -0,0 +1,86 @@
import os
from ..lib.audio import audio_to_bytes, bytes_to_audio, save_input_audio
import folder_paths
from ..lib.utils import get_filenames, get_hash, get_optimal_torch_device
from ..lib import BASE_CACHE_DIR, BASE_MODELS_DIR, karafan
from ..uvr5_cli import Separator
from .settings.downloader import KARAFAN_MODELS, MDX_MODELS, RVC_DOWNLOAD_LINK, VR_MODELS, download_file
temp_path = folder_paths.get_temp_directory()
cache_dir = os.path.join(BASE_CACHE_DIR,"uvr")
device = get_optimal_torch_device()
is_half = True
class UVR5Node:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
model_list = MDX_MODELS + VR_MODELS + KARAFAN_MODELS + get_filenames(root=BASE_MODELS_DIR,format_func=os.path.basename)
return {
"required": {
"audio": ("VHS_AUDIO",),
"model": (model_list,{
"default": "UVR/HP5-vocals+instrumentals.pth"
}),
"agg":("INT",{
"default": 10,
"min": 0, #Minimum value
"max": 20, #Maximum value
"step": 1, #Slider's step
"display": "slider"
}),
"format":(["wav", "flac", "mp3"],{
"default": "flac"
}),
},
"optional": {
"use_cache": ("BOOLEAN",{"default": True})
}
}
RETURN_TYPES = ("VHS_AUDIO","VHS_AUDIO","VHS_AUDIO")
RETURN_NAMES = ("primary_stem","secondary_stem","audio_passthrough")
FUNCTION = "split"
CATEGORY = "🌺RVC-Studio/uvr"
def split(self, audio, model, **kwargs):
filename = os.path.basename(model)
subfolder = os.path.dirname(model)
model_path = os.path.join(BASE_MODELS_DIR,subfolder,filename)
if not os.path.isfile(model_path):
download_link = f"{RVC_DOWNLOAD_LINK}{model}"
params = model_path, download_link
if download_file(params): print(f"successfully downloaded: {model_path}")
input_audio = bytes_to_audio(audio())
audio_path = os.path.join(temp_path,"uvr",f"{get_hash(audio(),model, *kwargs.items())}.wav")
if not os.path.isfile(audio_path):
os.makedirs(os.path.dirname(audio_path),exist_ok=True)
print(save_input_audio(audio_path,input_audio))
if "karafan" in model_path:
vocals, instrumental, input_audio = karafan.inference.Process(audio_path,cache_dir=cache_dir,**kwargs)
else:
model = Separator(
model_path=model_path,
device=device,
is_half="cuda" in str(device),
cache_dir=cache_dir,
**kwargs
)
vocals, instrumental, input_audio = model.run_inference(audio_path,format=kwargs.get("format"))
return (lambda:audio_to_bytes(*vocals), lambda:audio_to_bytes(*instrumental), audio)
@classmethod
def IS_CHANGED(cls, *args, **kwargs):
print(f"{args=} {kwargs=}")
return get_hash(*args, *kwargs.items())