voice-changer/server/voice_changer/RVC/RVC.py

433 lines
16 KiB
Python
Raw Normal View History

2023-04-05 20:31:10 +03:00
import sys
import os
2023-05-04 17:50:52 +03:00
import resampy
from dataclasses import asdict
from typing import cast
import numpy as np
import torch
2023-05-17 20:51:40 +03:00
from ModelSample import getModelSamples
from voice_changer.RVC.SampleDownloader import downloadModelFiles
2023-05-02 14:57:12 +03:00
2023-05-02 16:29:28 +03:00
2023-05-02 14:57:12 +03:00
# avoiding parse arg error in RVC
sys.argv = ["MMVCServerSIO.py"]
if sys.platform.startswith("darwin"):
baseDir = [x for x in sys.path if x.endswith("Contents/MacOS")]
if len(baseDir) != 1:
print("baseDir should be only one ", baseDir)
sys.exit()
modulePath = os.path.join(baseDir[0], "RVC")
sys.path.append(modulePath)
else:
sys.path.append("RVC")
2023-05-04 17:50:52 +03:00
2023-05-04 07:09:13 +03:00
from voice_changer.RVC.modelMerger.MergeModel import merge_model
from voice_changer.RVC.modelMerger.MergeModelRequest import MergeModelRequest
2023-05-02 14:57:12 +03:00
from voice_changer.RVC.ModelSlotGenerator import generateModelSlot
2023-04-28 01:36:08 +03:00
from voice_changer.RVC.RVCSettings import RVCSettings
2023-05-02 06:11:00 +03:00
from voice_changer.RVC.embedder.EmbedderManager import EmbedderManager
2023-05-11 13:36:36 +03:00
from voice_changer.utils.LoadModelParams import LoadModelParams
2023-04-28 02:01:15 +03:00
from voice_changer.utils.VoiceChangerModel import AudioInOut
2023-04-27 17:38:25 +03:00
from voice_changer.utils.VoiceChangerParams import VoiceChangerParams
2023-05-04 09:20:36 +03:00
from voice_changer.RVC.onnxExporter.export2onnx import export2onnx
2023-05-04 17:50:52 +03:00
from voice_changer.RVC.pitchExtractor.PitchExtractorManager import PitchExtractorManager
from voice_changer.RVC.pipeline.PipelineGenerator import createPipeline
from voice_changer.RVC.deviceManager.DeviceManager import DeviceManager
from voice_changer.RVC.pipeline.Pipeline import Pipeline
2023-04-13 02:00:28 +03:00
2023-05-04 17:50:52 +03:00
from Exceptions import NoModeLoadedException
2023-05-20 09:54:00 +03:00
from const import RVC_MAX_SLOT_NUM, RVC_MODEL_DIRNAME, SAMPLES_JSONS, UPLOAD_DIR
2023-05-14 22:24:58 +03:00
import shutil
import json
2023-04-05 20:31:10 +03:00
2023-04-27 17:38:25 +03:00
providers = [
"OpenVINOExecutionProvider",
"CUDAExecutionProvider",
"DmlExecutionProvider",
"CPUExecutionProvider",
]
2023-04-05 20:31:10 +03:00
class RVC:
2023-05-03 07:14:00 +03:00
initialLoad: bool = True
settings: RVCSettings = RVCSettings()
2023-05-04 11:15:53 +03:00
pipeline: Pipeline | None = None
2023-05-03 07:14:00 +03:00
2023-05-02 19:11:03 +03:00
deviceManager = DeviceManager.get_instance()
2023-04-28 02:01:15 +03:00
2023-05-03 07:14:00 +03:00
audio_buffer: AudioInOut | None = None
prevVol: float = 0
params: VoiceChangerParams
2023-05-20 09:54:00 +03:00
currentSlot: int = 0
2023-05-03 07:14:00 +03:00
needSwitch: bool = False
2023-04-27 17:38:25 +03:00
def __init__(self, params: VoiceChangerParams):
2023-05-02 16:29:28 +03:00
self.pitchExtractor = PitchExtractorManager.getPitchExtractor(
self.settings.f0Detector
)
2023-04-05 20:31:10 +03:00
self.params = params
2023-05-04 16:46:42 +03:00
EmbedderManager.initialize(params)
2023-05-14 22:24:58 +03:00
self.loadSlots()
2023-04-05 20:31:10 +03:00
print("RVC initialization: ", params)
2023-05-20 09:54:00 +03:00
sampleJsons: list[str] = []
for url in SAMPLES_JSONS:
filename = os.path.basename(url)
sampleJsons.append(filename)
sampleModels = getModelSamples(sampleJsons, "RVC")
2023-05-16 04:38:23 +03:00
if sampleModels is not None:
self.settings.sampleModels = sampleModels
# 起動時にスロットにモデルがある場合はロードしておく
if len(self.settings.modelSlots) > 0:
for i, slot in enumerate(self.settings.modelSlots):
if len(slot.modelFile) > 0:
self.prepareModel(i)
self.settings.modelSlotIndex = i
self.switchModel()
self.initialLoad = False
break
def getSampleInfo(self, id: str):
sampleInfos = list(filter(lambda x: x.id == id, self.settings.sampleModels))
if len(sampleInfos) > 0:
return sampleInfos[0]
else:
None
2023-05-17 07:09:35 +03:00
def moveToModelDir(self, file: str, dstDir: str):
dst = os.path.join(dstDir, os.path.basename(file))
if os.path.exists(dst):
os.remove(dst)
shutil.move(file, dst)
return dst
2023-04-28 00:39:51 +03:00
def loadModel(self, props: LoadModelParams):
2023-05-02 14:57:12 +03:00
target_slot_idx = props.slot
2023-05-07 23:51:24 +03:00
params = props.params
2023-05-08 19:01:20 +03:00
2023-05-16 04:38:23 +03:00
print("loadModel", params)
2023-05-17 07:09:35 +03:00
# サンプルが指定されたときはダウンロードしてメタデータをでっちあげる
2023-05-16 04:38:23 +03:00
if len(params["sampleId"]) > 0:
sampleInfo = self.getSampleInfo(params["sampleId"])
if sampleInfo is None:
print("[Voice Changer] sampleInfo is None")
return
2023-05-17 20:51:40 +03:00
modelPath, indexPath, featurePath = downloadModelFiles(sampleInfo)
2023-05-16 04:38:23 +03:00
params["files"]["rvcModel"] = modelPath
if indexPath is not None:
params["files"]["rvcIndex"] = indexPath
if featurePath is not None:
params["files"]["rvcFeature"] = featurePath
params["credit"] = sampleInfo.credit
params["description"] = sampleInfo.description
params["name"] = sampleInfo.name
params["sampleId"] = sampleInfo.id
2023-05-17 16:18:28 +03:00
params["termsOfUseUrl"] = sampleInfo.termsOfUseUrl
2023-05-20 09:54:00 +03:00
params["sampleRate"] = sampleInfo.sampleRate
params["modelType"] = sampleInfo.modelType
params["f0"] = sampleInfo.f0
2023-05-17 07:09:35 +03:00
# メタデータを見て、永続化モデルフォルダに移動させる
# その際に、メタデータのファイル格納場所も書き換える
2023-05-14 22:24:58 +03:00
slotDir = os.path.join(
self.params.model_dir, RVC_MODEL_DIRNAME, str(target_slot_idx)
2023-04-28 01:36:08 +03:00
)
2023-05-17 07:09:35 +03:00
os.makedirs(slotDir, exist_ok=True)
modelDst = self.moveToModelDir(params["files"]["rvcModel"], slotDir)
params["files"]["rvcModel"] = modelDst
2023-05-14 22:24:58 +03:00
if "rvcFeature" in params["files"]:
2023-05-17 07:09:35 +03:00
featureDst = self.moveToModelDir(params["files"]["rvcFeature"], slotDir)
params["files"]["rvcFeature"] = featureDst
2023-05-14 22:24:58 +03:00
if "rvcIndex" in params["files"]:
2023-05-17 07:09:35 +03:00
indexDst = self.moveToModelDir(params["files"]["rvcIndex"], slotDir)
params["files"]["rvcIndex"] = indexDst
2023-05-14 22:24:58 +03:00
json.dump(params, open(os.path.join(slotDir, "params.json"), "w"))
self.loadSlots()
2023-05-16 04:38:23 +03:00
# 初回のみロード(起動時にスロットにモデルがあった場合はinitialLoadはFalseになっている)
if self.initialLoad:
self.prepareModel(target_slot_idx)
self.settings.modelSlotIndex = target_slot_idx
self.switchModel()
self.initialLoad = False
elif target_slot_idx == self.currentSlot:
self.prepareModel(target_slot_idx)
2023-04-05 20:31:10 +03:00
return self.get_info()
2023-05-14 22:24:58 +03:00
def loadSlots(self):
dirname = os.path.join(self.params.model_dir, RVC_MODEL_DIRNAME)
self.settings.modelSlots = []
if not os.path.exists(dirname):
return
for slot_idx in range(RVC_MAX_SLOT_NUM):
slotDir = os.path.join(
self.params.model_dir, RVC_MODEL_DIRNAME, str(slot_idx)
)
modelSlot = generateModelSlot(slotDir)
self.settings.modelSlots.append(modelSlot)
2023-04-28 02:01:15 +03:00
def update_settings(self, key: str, val: int | float | str):
2023-05-02 14:57:12 +03:00
if key in self.settings.intData:
2023-05-03 07:14:00 +03:00
# 設定前処理
2023-04-28 02:01:15 +03:00
val = cast(int, val)
2023-04-21 09:48:12 +03:00
if key == "modelSlotIndex":
2023-05-03 07:14:00 +03:00
if val < 0:
2023-04-29 01:05:44 +03:00
return True
2023-05-03 07:14:00 +03:00
val = val % 1000 # Quick hack for same slot is selected
2023-05-12 12:43:02 +03:00
if (
self.settings.modelSlots[val].modelFile is None
or self.settings.modelSlots[val].modelFile == ""
):
2023-05-11 13:36:36 +03:00
print("[Voice Changer] slot does not have model.")
return True
2023-04-25 09:01:19 +03:00
self.prepareModel(val)
2023-05-03 07:14:00 +03:00
# 設定
setattr(self.settings, key, val)
2023-05-04 11:15:53 +03:00
if key == "gpu":
2023-05-03 07:14:00 +03:00
dev = self.deviceManager.getDevice(val)
half = self.deviceManager.halfPrecisionAvailable(val)
# half-precisionの使用可否が変わるときは作り直し
2023-05-04 11:15:53 +03:00
if self.pipeline is not None and self.pipeline.isHalf == half:
2023-05-03 07:14:00 +03:00
print(
2023-05-03 11:12:40 +03:00
"USE EXSISTING PIPELINE",
2023-05-03 07:14:00 +03:00
half,
)
2023-05-04 11:15:53 +03:00
self.pipeline.setDevice(dev)
2023-05-03 07:14:00 +03:00
else:
2023-05-03 11:12:40 +03:00
print("CHAGE TO NEW PIPELINE", half)
2023-05-03 07:14:00 +03:00
self.prepareModel(self.settings.modelSlotIndex)
2023-05-04 17:50:52 +03:00
if key == "enableDirectML":
if self.pipeline is not None and val == 0:
self.pipeline.setDirectMLEnable(False)
elif self.pipeline is not None and val == 1:
self.pipeline.setDirectMLEnable(True)
2023-04-05 20:31:10 +03:00
elif key in self.settings.floatData:
setattr(self.settings, key, float(val))
elif key in self.settings.strData:
setattr(self.settings, key, str(val))
2023-05-04 11:15:53 +03:00
if key == "f0Detector" and self.pipeline is not None:
pitchExtractor = PitchExtractorManager.getPitchExtractor(
2023-05-04 06:21:34 +03:00
self.settings.f0Detector
)
2023-05-04 11:15:53 +03:00
self.pipeline.setPitchExtractor(pitchExtractor)
2023-04-05 20:31:10 +03:00
else:
return False
return True
2023-05-04 17:50:52 +03:00
def prepareModel(self, slot: int):
if slot < 0:
2023-05-20 09:54:00 +03:00
print("[Voice Changer] Prepare Model of slot skip:", slot)
2023-05-04 17:50:52 +03:00
return self.get_info()
modelSlot = self.settings.modelSlots[slot]
print("[Voice Changer] Prepare Model of slot:", slot)
# pipelineの生成
self.next_pipeline = createPipeline(
modelSlot, self.settings.gpu, self.settings.f0Detector
)
# その他の設定
2023-05-17 06:37:35 +03:00
self.next_trans = modelSlot.defaultTune
self.next_index_ratio = modelSlot.defaultIndexRatio
2023-05-04 17:50:52 +03:00
self.next_samplingRate = modelSlot.samplingRate
self.next_framework = "ONNX" if modelSlot.isONNX else "PyTorch"
self.needSwitch = True
print("[Voice Changer] Prepare done.")
return self.get_info()
def switchModel(self):
print("[Voice Changer] Switching model..")
self.pipeline = self.next_pipeline
self.settings.tran = self.next_trans
2023-05-17 06:37:35 +03:00
self.settings.indexRatio = self.next_index_ratio
2023-05-04 17:50:52 +03:00
self.settings.modelSamplingRate = self.next_samplingRate
self.settings.framework = self.next_framework
print(
"[Voice Changer] Switching model..done",
)
2023-04-05 20:31:10 +03:00
def get_info(self):
data = asdict(self.settings)
return data
def get_processing_sampling_rate(self):
return self.settings.modelSamplingRate
2023-04-05 20:31:10 +03:00
2023-04-27 17:38:25 +03:00
def generate_input(
2023-04-28 02:01:15 +03:00
self,
newData: AudioInOut,
inputSize: int,
crossfadeSize: int,
solaSearchFrame: int = 0,
2023-04-27 17:38:25 +03:00
):
2023-04-14 03:18:34 +03:00
newData = newData.astype(np.float32) / 32768.0
2023-04-28 02:01:15 +03:00
if self.audio_buffer is not None:
# 過去のデータに連結
self.audio_buffer = np.concatenate([self.audio_buffer, newData], 0)
2023-04-14 03:18:34 +03:00
else:
self.audio_buffer = newData
2023-04-27 17:38:25 +03:00
convertSize = (
inputSize + crossfadeSize + solaSearchFrame + self.settings.extraConvertSize
)
2023-04-14 03:18:34 +03:00
if convertSize % 128 != 0: # モデルの出力のホップサイズで切り捨てが発生するので補う。
convertSize = convertSize + (128 - (convertSize % 128))
2023-04-28 02:01:15 +03:00
convertOffset = -1 * convertSize
self.audio_buffer = self.audio_buffer[convertOffset:] # 変換対象の部分だけ抽出
2023-04-14 03:18:34 +03:00
2023-04-28 02:01:15 +03:00
# 出力部分だけ切り出して音量を確認。(TODO:段階的消音にする)
cropOffset = -1 * (inputSize + crossfadeSize)
cropEnd = -1 * (crossfadeSize)
crop = self.audio_buffer[cropOffset:cropEnd]
2023-04-14 03:18:34 +03:00
rms = np.sqrt(np.square(crop).mean(axis=0))
vol = max(rms, self.prevVol * 0.0)
self.prevVol = vol
2023-04-05 20:31:10 +03:00
return (self.audio_buffer, convertSize, vol)
def inference(self, data):
2023-04-25 09:01:19 +03:00
if self.settings.modelSlotIndex < 0:
2023-04-27 17:38:25 +03:00
print(
"[Voice Changer] wait for loading model...",
self.settings.modelSlotIndex,
self.currentSlot,
)
2023-04-24 21:03:38 +03:00
raise NoModeLoadedException("model_common")
2023-05-03 07:14:00 +03:00
if self.needSwitch:
2023-05-03 11:12:40 +03:00
print(
f"[Voice Changer] Switch model {self.currentSlot} -> {self.settings.modelSlotIndex}"
)
self.currentSlot = self.settings.modelSlotIndex
2023-04-22 01:57:51 +03:00
self.switchModel()
2023-05-03 07:14:00 +03:00
self.needSwitch = False
2023-04-21 13:20:46 +03:00
2023-05-03 07:14:00 +03:00
half = self.deviceManager.halfPrecisionAvailable(self.settings.gpu)
2023-05-02 19:11:03 +03:00
audio = data[0]
convertSize = data[1]
vol = data[2]
audio = resampy.resample(audio, self.settings.modelSamplingRate, 16000)
if vol < self.settings.silentThreshold:
return np.zeros(convertSize).astype(np.int16)
2023-05-03 07:14:00 +03:00
repeat = 3 if half else 1
2023-05-02 19:11:03 +03:00
repeat *= self.settings.rvcQuality # 0 or 3
sid = 0
f0_up_key = self.settings.tran
index_rate = self.settings.indexRatio
if_f0 = 1 if self.settings.modelSlots[self.currentSlot].f0 else 0
embChannels = self.settings.modelSlots[self.currentSlot].embChannels
2023-05-03 07:14:00 +03:00
2023-05-04 11:15:53 +03:00
audio_out = self.pipeline.exec(
2023-05-02 19:11:03 +03:00
sid,
audio,
f0_up_key,
index_rate,
if_f0,
2023-05-04 11:15:53 +03:00
self.settings.extraConvertSize / self.settings.modelSamplingRate,
embChannels,
repeat,
2023-05-02 19:11:03 +03:00
)
result = audio_out * np.sqrt(vol)
return result
2023-04-05 20:31:10 +03:00
2023-04-10 18:21:17 +03:00
def __del__(self):
2023-05-04 11:15:53 +03:00
del self.pipeline
2023-04-10 18:21:17 +03:00
2023-04-29 01:05:44 +03:00
print("---------- REMOVING ---------------")
2023-04-10 18:21:17 +03:00
remove_path = os.path.join("RVC")
2023-04-28 02:01:15 +03:00
sys.path = [x for x in sys.path if x.endswith(remove_path) is False]
2023-04-10 18:21:17 +03:00
for key in list(sys.modules):
val = sys.modules.get(key)
try:
file_path = val.__file__
2023-04-11 01:37:39 +03:00
if file_path.find("RVC" + os.path.sep) >= 0:
2023-04-10 18:21:17 +03:00
print("remove", key, file_path)
sys.modules.pop(key)
2023-04-29 01:05:44 +03:00
except Exception: # type:ignore
# print(e)
2023-04-10 18:21:17 +03:00
pass
2023-04-13 02:00:28 +03:00
def export2onnx(self):
2023-05-03 11:12:40 +03:00
modelSlot = self.settings.modelSlots[self.settings.modelSlotIndex]
2023-04-22 09:12:10 +03:00
2023-05-08 19:01:20 +03:00
if modelSlot.isONNX:
2023-04-13 02:00:28 +03:00
print("[Voice Changer] export2onnx, No pyTorch filepath.")
2023-04-28 02:01:15 +03:00
return {"status": "ng", "path": ""}
2023-04-13 02:00:28 +03:00
2023-05-04 09:20:36 +03:00
output_file_simple = export2onnx(self.settings.gpu, modelSlot)
2023-04-27 17:38:25 +03:00
return {
"status": "ok",
"path": f"/tmp/{output_file_simple}",
"filename": output_file_simple,
}
2023-04-30 20:34:01 +03:00
def merge_models(self, request: str):
print("[Voice Changer] MergeRequest:", request)
req: MergeModelRequest = MergeModelRequest.from_json(request)
merged = merge_model(req)
targetSlot = 0
if req.slot < 0:
targetSlot = len(self.settings.modelSlots) - 1
else:
targetSlot = req.slot
2023-05-17 07:09:35 +03:00
# いったんは、アップロードフォルダに格納する。(歴史的経緯)
# 後続のloadmodelを呼び出すことで永続化モデルフォルダに移動させられる。
2023-04-30 20:34:01 +03:00
storeDir = os.path.join(UPLOAD_DIR, f"{targetSlot}")
print("[Voice Changer] store merged model to:", storeDir)
os.makedirs(storeDir, exist_ok=True)
storeFile = os.path.join(storeDir, "merged.pth")
torch.save(merged, storeFile)
2023-05-17 07:09:35 +03:00
# loadmodelを呼び出して永続化モデルフォルダに移動させる。
2023-05-17 06:37:35 +03:00
params = {
"defaultTune": req.defaultTune,
"defaultIndexRatio": req.defaultIndexRatio,
"sampleId": "",
"files": {"rvcModel": storeFile},
}
2023-04-30 20:34:01 +03:00
props: LoadModelParams = LoadModelParams(
2023-05-10 20:55:20 +03:00
slot=targetSlot, isHalf=True, params=params
2023-04-30 20:34:01 +03:00
)
self.loadModel(props)
self.prepareModel(targetSlot)
self.settings.modelSlotIndex = targetSlot
self.currentSlot = self.settings.modelSlotIndex
2023-05-20 22:21:54 +03:00
def update_model_default(self):
print("[voiceeeeee] UPDATE MODEL DEFAULT!!")
slotDir = os.path.join(
self.params.model_dir, RVC_MODEL_DIRNAME, str(self.currentSlot)
)
params = json.load(
open(os.path.join(slotDir, "params.json"), "r", encoding="utf-8")
)
params["defaultTune"] = self.settings.tran
params["defaultIndexRatio"] = self.settings.indexRatio
json.dump(params, open(os.path.join(slotDir, "params.json"), "w"))
self.loadSlots()