Files
smthemex 3aa602a6af Update MooER_node.py
add 80K ASR support
2024-09-03 21:33:13 +08:00

348 lines
14 KiB
Python

# !/usr/bin/env python
# -*- coding: UTF-8 -*-
import os
import subprocess
import sys
from huggingface_hub import snapshot_download
import time
import torchaudio
from torchaudio.transforms import Resample
from .mooer.datasets.speech_processor import *
from .mooer.configs import asr_config
from .mooer.models import mooer_model
from .mooer.utils.utils import *
import folder_paths
import platform
MAX_SEED = np.iinfo(np.int32).max
current_path = os.path.dirname(os.path.abspath(__file__))
node_path_dir = os.path.dirname(current_path)
comfy_file_path = os.path.dirname(node_path_dir)
diff_current_path = os.path.join(folder_paths.models_dir, "diffusers")
model_config = asr_config.ModelConfig()
logger = logging.getLogger()
logger.setLevel(logging.INFO)
device = str(get_device())
dtype = torch.float16
ffmpeg_path = os.getenv('FFMPEG_PATH')
if ffmpeg_path is None and platform.system() in ['Linux', 'Darwin']:
try:
result = subprocess.run(['which', 'ffmpeg'], capture_output=True, text=True)
if result.returncode == 0:
ffmpeg_path = result.stdout.strip()
print(f"FFmpeg is installed at: {ffmpeg_path}")
else:
print("FFmpeg is not installed. Please download ffmpeg-static and export to FFMPEG_PATH.")
print("For example: export FFMPEG_PATH=/any_path/ffmpeg-4.4-amd64-static")
except Exception as e:
pass
if ffmpeg_path is not None and ffmpeg_path not in os.getenv('PATH'):
print("Adding FFMPEG_PATH to PATH")
os.environ["PATH"] = f"{ffmpeg_path}:{os.environ['PATH']}"
mooer_file_dir= os.path.join(folder_paths.input_directory,"mooer_files")
if not os.path.exists(mooer_file_dir):
os.makedirs(mooer_file_dir)
def find_directories(base_path):
directories = []
for root, dirs, files in os.walk(base_path):
for name in dirs:
directories.append(name)
return directories
audio_dir_list = find_directories(mooer_file_dir)
if audio_dir_list:
audio_dir_list = ["none"] + audio_dir_list
else:
audio_dir_list = ["none", ]
def get_local_path(model_path):
path = os.path.join(diff_current_path, model_path)
model_path = os.path.normpath(path)
if sys.platform == 'win32':
model_path = model_path.replace('\\', "/")
return model_path
def get_instance_path(path):
instance_path = os.path.normpath(path)
if sys.platform == 'win32':
instance_path = instance_path.replace('\\', "/")
return instance_path
dif_paths = []
for search_path in folder_paths.get_folder_paths("diffusers"):
if os.path.exists(search_path):
for root, subdir, files in os.walk(search_path, followlinks=True):
if "configuration.json" in files:
dif_paths.append(os.path.relpath(root, start=search_path))
if dif_paths:
dif_paths = ["none"] + [x for x in dif_paths if "Qwen2-7B-Instruct" in x or "MooER-MTL-5K" in x]
else:
dif_paths = ["none", ]
def instance_path(path, repo):
if repo == "":
if path == "none":
repo = "none"
else:
model_path = get_local_path(path)
repo = get_instance_path(model_path)
return repo
def process_wav(audio_raw,sample_rate,adapter_downsample_rate,cmvn,tokenizer,prompt_template,prompt_org):
if sample_rate != 16000:
# resample the data
resampler = Resample(orig_freq=sample_rate, new_freq=16000)
audio_raw = resampler(audio_raw)
if audio_raw.shape[0] > 1:
# convert to mono
audio_raw = audio_raw.mean(dim=0, keepdim=True)
audio_raw = audio_raw[0]
prompt = prompt_template.format(prompt_org)
audio_mel = compute_fbank(waveform=audio_raw)
audio_mel = apply_lfr(inputs=audio_mel, lfr_m=7, lfr_n=6)
audio_mel = apply_cmvn(audio_mel, cmvn=cmvn)
audio_length = audio_mel.shape[0]
audio_length = audio_length // adapter_downsample_rate
audio_pseudo = torch.full((audio_length,), -1)
prompt_ids = tokenizer.encode(prompt)
prompt_length = len(prompt_ids)
prompt_ids = torch.tensor(prompt_ids, dtype=torch.int64)
example_ids = torch.cat((audio_pseudo, prompt_ids)) # [audio, prompt]
example_mask = example_ids.ge(-1)
items = {
"input_ids": example_ids,
"attention_mask": example_mask,
"audio_mel": audio_mel,
"audio_length": audio_length,
"prompt_length": prompt_length,
}
return items
@torch.no_grad()
def process_main(model,tokenizer,cmvn,adapter_downsample_rate,audio_raw,sample_rate,audio_dir,prompt_template,prompt_org,context_scope):
audio_suffix=["wav","mpa3","flac"]
if audio_dir!="none":
real_audio_dir = os.path.join(mooer_file_dir, audio_dir) # audio list in
file_list=os.listdir(real_audio_dir) #file list
if len(file_list)>=1:
batch_wav_paths=[os.path.join(real_audio_dir,i) for i in file_list if i.rsplit(".")[-1] in audio_suffix ]
num_batches=len(batch_wav_paths)
infer_time = []
ASR_out = []
AST_out = []
for i in range(num_batches):
samples = []
for wav_path in batch_wav_paths:
audio_raw, sample_rate = torchaudio.load(wav_path)
samples.append(process_wav(audio_raw, sample_rate, adapter_downsample_rate, cmvn, tokenizer,prompt_template,prompt_org))
print(wav_path.rsplit(audio_dir)[-1])
batch = process_batch(samples, tokenizer=tokenizer)
for key in batch.keys():
batch[key] = batch[key].to(device) if isinstance(batch[key], torch.Tensor) else batch[key]
with context_scope(dtype=dtype):
ss = time.perf_counter()
model_outputs = model.generate(**batch)
infer_time.append(time.perf_counter() - ss)
logging.info(f"Infer time: {time.perf_counter() - ss}")
output_text = model.tokenizer.batch_decode(model_outputs, add_special_tokens=False,
skip_special_tokens=True)
for idx, text in enumerate(output_text):
text = text.split('\n')
if len(text) == 2:
ASR_out.append([text[0].strip()])
AST_out.append([text[1].strip()])
else:
ASR_out.append([text[0].strip()])
logging.info(sum(infer_time))
else:
print(f"No audio fiels in {real_audio_dir}")
raise "No audio fiels in audio_dir... "
else:
items = process_wav(audio_raw, sample_rate, adapter_downsample_rate, cmvn, tokenizer,prompt_template,prompt_org)
batch = process_batch([items], tokenizer=tokenizer)
for key in batch.keys():
batch[key] = batch[key].to(device) if isinstance(batch[key], torch.Tensor) else batch[key]
with context_scope(dtype=dtype):
ss = time.perf_counter()
model_outputs = model.generate(**batch)
logging.info(f"Infer time: {time.perf_counter() - ss}")
output_text = model.tokenizer.batch_decode(model_outputs, add_special_tokens=False,
skip_special_tokens=True)
ASR_out=[]
AST_out = []
for text in output_text:
text = text.split('\n')
if len(text) == 2:
ASR_out.append([text[0].strip()])
AST_out.append([text[1].strip()])
else:
ASR_out.append([text[0].strip()])
ASR_text=' '.join(ASR_out[0]) if ASR_out else ("need asr or asr_ast.")
AST_text = ' '.join(AST_out[0]) if AST_out else "need ast or asr_ast."
return ASR_text,AST_text
class MooER_LoadModel:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"Qwen2_repo":("STRING", {"default": "Qwen/Qwen2-7B-Instruct"}),
"Qwen2_diff":(dif_paths,),
"mooer_repo": ("STRING", {"default": "mtspeech/MooER-MTL-5K"}),
"mooer_diff":(dif_paths,),
"use_torch_musa":("BOOLEAN", {"default": False},),
"use_modelscope":("BOOLEAN", {"default": False},),
"encoder_name":(["paraformer","whisper","hubert","paraformer"],),
"mode_choice":(["ASR_AST","ASR","AST"],),
}
}
RETURN_TYPES = ("MODEL","MODEL","MODEL",)
RETURN_NAMES = ("model","tokenizer","cmvn",)
FUNCTION = "main_loader"
CATEGORY = "MooER"
def instance_path(self,repo,path):
if repo == "":
if path == "none":
raise "need fill repo or local model path.."
else:
repo = get_instance_path(get_local_path(path))
elif repo and path!="none":
repo = get_instance_path(get_local_path(path))
return repo
def main_loader(self,Qwen2_repo,Qwen2_diff,mooer_repo,mooer_diff,use_torch_musa,use_modelscope,encoder_name,mode_choice):
logger.info("Run on {}".format(device.upper()))
if use_torch_musa:
try:
import torch_musa
except ImportError as e:
print(f"You should install torch_musa if you want to run on Moore Threads GPU,{e}")
cache_dir = os.path.join(diff_current_path, "cache")
# pre models
qwen2_repo_id=self.instance_path(Qwen2_repo,Qwen2_diff)
local_llm_dir = os.path.join(diff_current_path, "Qwen/Qwen2-7B-Instruct")
if os.path.exists(local_llm_dir) and qwen2_repo_id=="Qwen/Qwen2-7B-Instruct":
raise "fill'Qwen/Qwen2-7B-Instruct' will automatic download and And cover existing models"
if qwen2_repo_id=="Qwen2-7B-Instruct":
if use_modelscope:
from modelscope.hub.snapshot_download import snapshot_download as snapshot_download_mo
print("download from modelscope...")
qwen2_repo_id=snapshot_download_mo("qwen/qwen2-7b-instruct",local_dir=local_llm_dir, )
else:
print("download from huggingface...")
qwen2_repo_id=snapshot_download("Qwen/Qwen2-7B-Instruct",cache_dir=cache_dir,local_dir=local_llm_dir, local_dir_use_symlinks=False)
mooer_repo_id = self.instance_path(mooer_repo,mooer_diff)
local_mo_dir = os.path.join(diff_current_path, "mtspeech/MooER-MTL-5K")
if os.path.exists(local_mo_dir) and mooer_repo_id=="mtspeech/MooER-MTL-5K":
raise "fill'mtspeech/MooER-MTL-5K' will automatic download and And cover existing models"
if mooer_repo_id=="mtspeech/MooER-MTL-5K":
if use_modelscope:
ignore_files = ["model-00001-of-00004.safetensors", "model-00002-of-00004.safetensors",
"model-00003-of-00004.safetensors", "model-00004-of-00004.safetensors", ]
from modelscope.hub.snapshot_download import snapshot_download as snapshot_download_mo
print("download from modelscope...")
mooer_repo_id=snapshot_download_mo("MooreThreadsSpeech/MooER-MTL-5K", ignore_file_pattern=ignore_files,
local_dir=local_mo_dir, )
else:
print("download from huggingface...")
mooer_repo_id=snapshot_download("mtspeech/MooER-MTL-5K", cache_dir=cache_dir,local_dir=local_mo_dir,local_dir_use_symlinks=False )
elif mooer_repo_id=="mtspeech/MooER-MTL-80K":
if use_modelscope:
ignore_files = ["model-00001-of-00004.safetensors", "model-00002-of-00004.safetensors",
"model-00003-of-00004.safetensors", "model-00004-of-00004.safetensors", ]
from modelscope.hub.snapshot_download import snapshot_download as snapshot_download_mo
print("download from modelscope...")
mooer_repo_id=snapshot_download_mo("MooreThreadsSpeech/MooER-MTL-80K", ignore_file_pattern=ignore_files,
local_dir=local_mo_dir, )
else:
print("download from huggingface...")
mooer_repo_id=snapshot_download("mtspeech/MooER-MTL-80K", cache_dir=cache_dir,local_dir=local_mo_dir,local_dir_use_symlinks=False )
model, tokenizer = mooer_model.init_model(
model_config,qwen2_repo_id,mooer_repo_id,encoder_name,mode_choice)
model.to(device)
model.eval()
cmvn = load_cmvn(os.path.join(diff_current_path,mooer_repo_id,"paraformer_encoder/am.mvn"))
return (model,tokenizer,cmvn,)
class MooER_Sampler:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"audio": ("AUDIO",),
"model":("MODEL",),
"tokenizer":("MODEL",),
"cmvn":("MODEL",),
"asr_prompt":("STRING", {"default": "Transcribe speech to text. "}),
"ast_prompt": ("STRING", {"default": "Translate speech to english text."}),
"audio_dir": (audio_dir_list,),
"adapter_downsample_rate": ("INT", {"default": 2, "min": 0, "max": 10}),
"prompt_key": (["asr","ast",],),
},
}
RETURN_TYPES = ("STRING","STRING",)
RETURN_NAMES = ("ASR_text","AST_text",)
FUNCTION = "em_main"
CATEGORY = "MooER"
def em_main(self,audio,model,tokenizer,cmvn,asr_prompt,ast_prompt,audio_dir,adapter_downsample_rate,prompt_key,):
# data process
PROMPT_TEMPLATE_DICT = {
'qwen': "<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n",}
PROMPT_DICT = {'asr': asr_prompt,'ast': ast_prompt,}
prompt_template_key = model_config.get('prompt_template_key', 'qwen')
prompt_template = PROMPT_TEMPLATE_DICT[prompt_template_key]
prompt_org = PROMPT_DICT[prompt_key]
context_scope = torch.musa.amp.autocast if 'musa' in device else torch.cuda.amp.autocast
# audio
audio_raw=audio["waveform"].squeeze(0)
sample_rate= audio["sample_rate"]
ASR_text,AST_text=process_main(model,tokenizer,cmvn,adapter_downsample_rate,audio_raw,sample_rate,audio_dir,prompt_template,prompt_org,context_scope)
torch.cuda.empty_cache()
return (ASR_text,AST_text,)
NODE_CLASS_MAPPINGS = {
"MooER_LoadModel":MooER_LoadModel,
"MooER_Sampler": MooER_Sampler
}
NODE_DISPLAY_NAME_MAPPINGS = {
"MooER_LoadModel":"MooER_LoadModel",
"MooER_Sampler": "MooER_Sampler",
}