본문 바로가기

서비스 제작

[RVC 코드 분석] 화자의 목소리를 받아서 화자 목소리 모델을 생성

화자의 목소리를 담은 노래 커버를 제작하는 ai 서비스를 개발중에 있다. 이를 위해 RVC(Retrieval-based Voice Conversion)라는 AI 음성 합성 기술을 사용한다. RVC에서 많이 사용하는 깃헙 레포지토리는 https://github.com/RVC-Project/Retrieval-based-Voice-Conversion-WebUI/tree/main 이다. 하지만 레포지토리 이름에서 보이는 대로 이는 그라디오로 만들어진 웹 상에서 작동하도록 설계된 레포지토리이다. 서비스 제작을 위해서는 코드를 분석하여 api로 만들 수 있도록 코드를 수정해야한다. 이 포스트에서는 서비스를 위한 AI 기능 구현을 위한 코드 분석 과정을 담는다.


서비스 상에서 구현되어야하는 AI 기능은 아래와 같다.
  • 노래에서 가수 목소리 음원과 MR 음원으로 분리
  • 화자의 목소리를 받아서 화자 목소리 모델을 생성
  • 화자 목소리 모델과 가수 목소리 음원을 합성하여 화자 목소리로 부른 음원을 생성

RVC 깃헙 코드상에서 이 기능들이 모두 구현되어 있다. 포스트 당 하나씩 살펴보자. 이번 포스팅에는 “화자의 목소리를 받아서 화자 목소리 모델을 생성”에 대해 알아본다.

화자의 목소리를 받아서 화자 목소리 모델을 생성

화자 목소리 모델을 훈련하는 것은 웹 상에서는 24번 “원클릭 훈련” 버튼을 클릭하면 데이터 전처리(8), 특징추출(12), 모델 훈련(22), 특징 인덱스 훈련(24) 이 4가지 과정을 거쳐서 화자 목소리 모델이 생성된다.

각 빨간색 네모 영역은 다음을 의미한다. 카테고리는 특정 네모 영역이 어떤 과정을 위한 것인지 표기하기 위해 나타낸다. 풀어 설명하면 스크린샷 번호가 카테고리 번호가 2으로 되어있다면 특징 추출에 그 스크린샷 번호 영역이 사용된다는 것이다. (이런 카테고리 번호가 붙어진 이유는 아래에 나온다.)


  • 1: 데이터 전처리
  • 2: 특징 추출
  • 3: 모델 훈련
  • 4: 특징 인덱스 훈련
스크린샷 번호 의미 카테고리
1 화자 목소리 프로젝트 이름 입력 1, 2, 3, 4
2 샘플링 ratio 1, 3
3 화자 목소리 파일이 음높이를 포함하는지.(화자 목소리가 노래로 부터 추출되었으면 “예”를 아니면 “아니오”를 선택) 2, 3
4 모델 버전 2, 3, 4
5 cpu 프로세스 수 1, 2
6 음성 파일이 담긴 폴더 경로 1
7 화자 ID 3
8 데이터 처리 실행 버튼 1
9 gpu 카드 번호 -
10 특징 추출 알고리즘 2
11 알고리즘 rmvpe 사용할 때의 프로세스 번호 2
12 특징 추출 실행 버튼 2
13 모델 저장 빈도 3
14 총 훈련 epoch 3
15 배치사이즈 3
16 디스크 공간을 절약하기 위해 최신 모델 파일만 저장할지 3
17 모든 훈련 세트를 VRAM에 캐시할지 여부. 10분 미만의 소량 데이터는 캐시하여 훈련 속도를 높일 수 있지만, 대량 데이터 캐시는 VRAM을 과부하시키고 속도를 크게 향상시키지 못함 3
18 저장시마다 모델을 weights 폴더에 저장할지 3
19 미리 훈련된 G 모델 경로 3
20 미리 훈련된 G 모델 경로 3
21 gpu 카드 번호 2, 3
22 모델 훈련 실행 버튼 3
23 특징 인덱스 훈련 실행 버튼 4
24 전체 훈련 실행 버튼 -

이제 이 스크린샷의 부분을 코드에서 찾아보자. 위와 같이 WEB을 표현하는 gradio가 담겨있는 파일은 infer-web.py이다. 24번(원클릭훈련) 버튼을 눌렀을 때의 코드는 아래와 같다. but5 버튼이 25번 영역인 전체 훈련 실행 버튼이다. 코드에 대해 간략하게 설명하면 but5를 클릭하면 train1key 함수에 exp_dir1, sr2등의 파라미터가 들어가서 실행된다. 그리고 그 결과물이 info3에 담기게 된다.


but5.click(
    train1key,
    [
        exp_dir1,
        sr2,
        if_f0_3,
        trainset_dir4,
        spk_id5,
        np7,
        f0method8,
        save_epoch10,
        total_epoch11,
        batch_size12,
        if_save_latest13,
        pretrained_G14,
        pretrained_D15,
        gpus16,
        if_cache_gpu17,
        if_save_every_weights18,
        version19,
        gpus_rmvpe,
    ],
    info3,
    api_name="train_start_all",
)

but5를 클릭하면 실행되는 train1key 함수를 살펴보자.


def train1key(
    exp_dir1,
    sr2,
    if_f0_3,
    trainset_dir4,
    spk_id5,
    np7,
    f0method8,
    save_epoch10,
    total_epoch11,
    batch_size12,
    if_save_latest13,
    pretrained_G14,
    pretrained_D15,
    gpus16,
    if_cache_gpu17,
    if_save_every_weights18,
    version19,
    gpus_rmvpe,
):
    infos = []

    def get_info_str(strr):
        infos.append(strr)
        return "\n".join(infos)

    # step1:处理数据
    yield get_info_str(i18n("step1:正在处理数据"))
    [get_info_str(_) for _ in preprocess_dataset(trainset_dir4, exp_dir1, sr2, np7)]

    # step2a:提取音高
    yield get_info_str(i18n("step2:正在提取音高&正在提取特征"))
    [
        get_info_str(_)
        for _ in extract_f0_feature(
            gpus16, np7, f0method8, if_f0_3, exp_dir1, version19, gpus_rmvpe
        )
    ]

    # step3a:训练模型
    yield get_info_str(i18n("step3a:正在训练模型"))
    click_train(
        exp_dir1,
        sr2,
        if_f0_3,
        spk_id5,
        save_epoch10,
        total_epoch11,
        batch_size12,
        if_save_latest13,
        pretrained_G14,
        pretrained_D15,
        gpus16,
        if_cache_gpu17,
        if_save_every_weights18,
        version19,
    )
    yield get_info_str(
        i18n("训练结束, 您可查看控制台训练日志或实验文件夹下的train.log")
    )

    # step3b:训练索引
    [get_info_str(_) for _ in train_index(exp_dir1, version19)]
    yield get_info_str(i18n("全流程结束!"))

주석을 확인하면 스크린샷에서 본 것 처럼 4 steps로 나눠져 있는 것을 확인할 수 있다.


step 번호 의미 함수 입력되는 파라미터
step 1 데이터 전처리 preprocess_dataset trainset_dir4, exp_dir1, sr2, np7
step 2 특징 추출 extract_f0_feature gpus16, np7, f0method8, if_f0_3, exp_dir1, version19, gpus_rmvpe
step 3-a 모델 훈련 click_train exp_dir1, sr2, if_f0_3, spk_id5, save_epoch10, total_epoch11, batch_size12, if_save_latest13, pretrained_G14, pretrained_D15, gpus16, if_cache_gpu17, if_save_every_weights18, version19
step 3-b 특징 인덱스 훈련 train_index exp_dir1, version19

이제 파라미터 변수명과 웹 스크린샷의 번호를 코드를 보고 연결해보면 아래와 같다.


스크린샷 번호 의미 카테고리 파라미터 변수명
1 화자 목소리 프로젝트 이름 입력 1, 2, 3, 4 exp_dir1
2 샘플링 ratio 1, 3 sr2
3 화자 목소리 파일이 음높이를 포함하는지.(화자 목소리가 노래로 부터 추출되었으면 “예”를 아니면 “아니오”를 선택) 2, 3 if_f0_3
4 모델 버전 2, 3, 4 version19
5 cpu 프로세스 수 1, 2 np7
6 음성 파일이 담긴 폴더 경로 1 trainset_dir4
7 화자 ID 3 spk_id5
8 데이터 처리 실행 버튼 1 but1
9 gpu 카드 번호 - gpus6
10 특징 추출 알고리즘 2 f0method8
11 알고리즘 rmvpe 사용할 때의 프로세스 번호 2 gpus_rmvpe
12 특징 추출 실행 버튼 2 but2
13 모델 저장 빈도 3 save_epoch10
14 총 훈련 epoch 3 total_epoch11
15 배치사이즈 3 batch_size12
16 디스크 공간을 절약하기 위해 최신 모델 파일만 저장할지 3 if_save_latest13
17 모든 훈련 세트를 VRAM에 캐시할지 여부. 10분 미만의 소량 데이터는 캐시하여 훈련 속도를 높일 수 있지만, 대량 데이터 캐시는 VRAM을 과부하시키고 속도를 크게 향상시키지 못함 3 if_cache_gpu17
18 저장시마다 모델을 weights 폴더에 저장할지 3 if_save_every_weights18
19 미리 훈련된 G 모델 경로 3 pretrained_G14
20 미리 훈련된 G 모델 경로 3 pretrained_D15
21 gpu 카드 번호 2, 3 gpus16
22 모델 훈련 실행 버튼 3 but3
23 특징 인덱스 훈련 실행 버튼 4 but4
24 전체 훈련 실행 버튼 - but5

이제 각 step에 해당하는 함수를 분석해보자.


데이터 전처리(preprocess_dataset 함수)

def preprocess_dataset(trainset_dir, exp_dir, sr, n_p):
    sr = sr_dict[sr]
    os.makedirs("%s/logs/%s" % (now_dir, exp_dir), exist_ok=True)
    f = open("%s/logs/%s/preprocess.log" % (now_dir, exp_dir), "w")
    f.close()
    cmd = '"%s" infer/modules/train/preprocess.py "%s" %s %s "%s/logs/%s" %s %.1f' % (
        config.python_cmd,
        trainset_dir,
        sr,
        n_p,
        now_dir,
        exp_dir,
        config.noparallel,
        config.preprocess_per,
    )
    logger.info("Execute: " + cmd)
    # , stdin=PIPE, stdout=PIPE,stderr=PIPE,cwd=now_dir
    p = Popen(cmd, shell=True)
    # 煞笔gr, popen read都非得全跑完了再一次性读取, 不用gr就正常读一句输出一句;只能额外弄出一个文本流定时读
    done = [False]
    threading.Thread(
        target=if_done,
        args=(
            done,
            p,
        ),
    ).start()
    while 1:
        with open("%s/logs/%s/preprocess.log" % (now_dir, exp_dir), "r") as f:
            yield (f.read())
        sleep(1)
        if done[0]:
            break
    with open("%s/logs/%s/preprocess.log" % (now_dir, exp_dir), "r") as f:
        log = f.read()
    logger.info(log)
    yield log

코드의 주요한 부분은 “infer/modules/train/preprocess.py”파일을 실행시킨다는 점이다. 저 preprocess.py의 코드까지는 서비스 제작을 위해서는 이해가 필요없기 때문에 데이터 전처리 코드 분석은 여기까지 진행한다. 서비스 제작을 위해서 약간의 수정을 거쳐서 아래와 같은 새로운 파일을 만들어주었다.


# 새롭게 작성된 preprocess.py
import os
import sys
import argparse
import logging
from time import sleep
from subprocess import Popen
import threading

# Directory setup
now_dir = os.getcwd()
sys.path.append(now_dir)

from configs.config import Config

# Set logging levels
logging.getLogger("numba").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
logger = logging.getLogger(__name__)

# Sampling rate dictionary
sr_dict = {
    "32k": 32000,
    "40k": 40000,
    "48k": 48000,
}

# Configuration from external file
config = Config()

def if_done(done, p):
    p.wait()
    done[0] = True

def preprocess_dataset(trainset_dir, exp_dir, sr, n_p):
    os.makedirs(trainset_dir, exist_ok=True)

    sr = sr_dict[sr]
    log_dir = f"{now_dir}/logs/{exp_dir}"
    os.makedirs(log_dir, exist_ok=True)

    cmd = f'"{config.python_cmd}" infer/modules/train/preprocess.py "{trainset_dir}" {sr} {n_p} "{log_dir}" {config.noparallel} {config.preprocess_per}'
    logger.info("Execute: " + cmd)
    p = Popen(cmd, shell=True)
    done = [False]
    threading.Thread(target=if_done, args=(done, p)).start()

def arg_parse():
    parser = argparse.ArgumentParser(description="Preprocess audio data for training.")
    parser.add_argument('--trainset_dir', type=str, default="/home/choi/desktop/rvc/ai/data/user1/input/speaker",
                        help="Directory where the training dataset is stored")
    parser.add_argument('--exp_dir', type=str, default="../../data/user1/output/trained_model",
                        help="Directory where the experiment's outputs are saved")
    parser.add_argument('--sampling_rate', type=str, choices=['32k', '40k', '48k'], default="40k",
                        help="Sampling rate for the audio processing")
    parser.add_argument('--n_p', type=int, default=8,
                        help="Number of processes to use")
    return parser.parse_args()

def main():
    args = arg_parse()
    preprocess_dataset(args.trainset_dir, args.exp_dir, args.sampling_rate, args.n_p)

if __name__ == "__main__":
    main()

특징 추출(extract_f0_feature 함수)


def extract_f0_feature(gpus, n_p, f0method, if_f0, exp_dir, version19, gpus_rmvpe):
    gpus = gpus.split("-")
    os.makedirs("%s/logs/%s" % (now_dir, exp_dir), exist_ok=True)
    f = open("%s/logs/%s/extract_f0_feature.log" % (now_dir, exp_dir), "w")
    f.close()
    if if_f0:
        if f0method != "rmvpe_gpu":
            cmd = (
                '"%s" infer/modules/train/extract/extract_f0_print.py "%s/logs/%s" %s %s'
                % (
                    config.python_cmd,
                    now_dir,
                    exp_dir,
                    n_p,
                    f0method,
                )
            )
            logger.info("Execute: " + cmd)
            p = Popen(
                cmd, shell=True, cwd=now_dir
            )  # , stdin=PIPE, stdout=PIPE,stderr=PIPE
            # 煞笔gr, popen read都非得全跑完了再一次性读取, 不用gr就正常读一句输出一句;只能额外弄出一个文本流定时读
            done = [False]
            threading.Thread(
                target=if_done,
                args=(
                    done,
                    p,
                ),
            ).start()
        else:
            if gpus_rmvpe != "-":
                gpus_rmvpe = gpus_rmvpe.split("-")
                leng = len(gpus_rmvpe)
                ps = []
                for idx, n_g in enumerate(gpus_rmvpe):
                    cmd = (
                        '"%s" infer/modules/train/extract/extract_f0_rmvpe.py %s %s %s "%s/logs/%s" %s '
                        % (
                            config.python_cmd,
                            leng,
                            idx,
                            n_g,
                            now_dir,
                            exp_dir,
                            config.is_half,
                        )
                    )
                    logger.info("Execute: " + cmd)
                    p = Popen(
                        cmd, shell=True, cwd=now_dir
                    )  # , shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=now_dir
                    ps.append(p)
                # 煞笔gr, popen read都非得全跑完了再一次性读取, 不用gr就正常读一句输出一句;只能额外弄出一个文本流定时读
                done = [False]
                threading.Thread(
                    target=if_done_multi,  #
                    args=(
                        done,
                        ps,
                    ),
                ).start()
            else:
                cmd = (
                    config.python_cmd
                    + ' infer/modules/train/extract/extract_f0_rmvpe_dml.py "%s/logs/%s" '
                    % (
                        now_dir,
                        exp_dir,
                    )
                )
                logger.info("Execute: " + cmd)
                p = Popen(
                    cmd, shell=True, cwd=now_dir
                )  # , shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=now_dir
                p.wait()
                done = [True]
        while 1:
            with open(
                "%s/logs/%s/extract_f0_feature.log" % (now_dir, exp_dir), "r"
            ) as f:
                yield (f.read())
            sleep(1)
            if done[0]:
                break
        with open("%s/logs/%s/extract_f0_feature.log" % (now_dir, exp_dir), "r") as f:
            log = f.read()
        logger.info(log)
        yield log
    # 对不同part分别开多进程
    """
    n_part=int(sys.argv[1])
    i_part=int(sys.argv[2])
    i_gpu=sys.argv[3]
    exp_dir=sys.argv[4]
    os.environ["CUDA_VISIBLE_DEVICES"]=str(i_gpu)
    """
    leng = len(gpus)
    ps = []
    for idx, n_g in enumerate(gpus):
        cmd = (
            '"%s" infer/modules/train/extract_feature_print.py %s %s %s %s "%s/logs/%s" %s %s'
            % (
                config.python_cmd,
                config.device,
                leng,
                idx,
                n_g,
                now_dir,
                exp_dir,
                version19,
                config.is_half,
            )
        )
        logger.info("Execute: " + cmd)
        p = Popen(
            cmd, shell=True, cwd=now_dir
        )  # , shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=now_dir
        ps.append(p)
    # 煞笔gr, popen read都非得全跑完了再一次性读取, 不用gr就正常读一句输出一句;只能额外弄出一个文本流定时读
    done = [False]
    threading.Thread(
        target=if_done_multi,
        args=(
            done,
            ps,
        ),
    ).start()
    while 1:
        with open("%s/logs/%s/extract_f0_feature.log" % (now_dir, exp_dir), "r") as f:
            yield (f.read())
        sleep(1)
        if done[0]:
            break
    with open("%s/logs/%s/extract_f0_feature.log" % (now_dir, exp_dir), "r") as f:
        log = f.read()
    logger.info(log)
    yield log

나는 특징 추출 알고리즘으로 rmvpe-gpu를 사용할 것이다. 그 관점에서 이 코드에서도 주요부분은 infer/modules/train/extract/extract_f0_rmvpe.py 파일을 실행한다는 것이다. 이번에도 서비스 제작을 위해서 코드 이해는 이 정도면 충분하기 때문에 더 깊게 들어가지 않는다. 그리고 위 코드의 내용을 필요한 부분만 정리해서 아래와 같이 새로운 파일을 만들었다.


import os
import sys
now_dir = os.getcwd()
sys.path.append(now_dir)

import argparse
import logging
from time import sleep
from subprocess import Popen
import threading

from configs.config import Config

logging.getLogger("numba").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)

logger = logging.getLogger(__name__)

config = Config()

def if_done(done, p):
    while 1:
        if p.poll() is None:
            sleep(0.5)
        else:
            break
    done[0] = True

def if_done_multi(done, ps):
    while 1:
        # poll==None代表进程未结束
        # 只要有一个进程未结束都不停
        flag = 1
        for p in ps:
            if p.poll() is None:
                flag = 0
                sleep(0.5)
                break
        if flag == 1:
            break
    done[0] = True

def extract_f0_feature(gpus, n_p, f0method, if_f0, exp_dir, version19, gpus_rmvpe):
    gpus = gpus.split("-")
    os.makedirs("%s/logs/%s" % (now_dir, exp_dir), exist_ok=True)
    f = open("%s/logs/%s/extract_f0_feature.log" % (now_dir, exp_dir), "w")
    f.close()
    if if_f0:
        if f0method != "rmvpe_gpu":
            cmd = (
                '"%s" infer/modules/train/extract/extract_f0_print.py "%s/logs/%s" %s %s'
                % (
                    config.python_cmd,
                    now_dir,
                    exp_dir,
                    n_p,
                    f0method,
                )
            )
            logger.info("Execute: " + cmd)
            p = Popen(
                cmd, shell=True, cwd=now_dir
            )  # , stdin=PIPE, stdout=PIPE,stderr=PIPE
            # 煞笔gr, popen read都非得全跑完了再一次性读取, 不用gr就正常读一句输出一句;只能额外弄出一个文本流定时读
            done = [False]
            threading.Thread(
                target=if_done,
                args=(
                    done,
                    p,
                ),
            ).start()
        else:
            if gpus_rmvpe != "-":
                gpus_rmvpe = gpus_rmvpe.split("-")
                leng = len(gpus_rmvpe)
                ps = []
                for idx, n_g in enumerate(gpus_rmvpe):
                    cmd = (
                        '"%s" infer/modules/train/extract/extract_f0_rmvpe.py %s %s %s "%s/logs/%s" %s '
                        % (
                            config.python_cmd,
                            leng,
                            idx,
                            n_g,
                            now_dir,
                            exp_dir,
                            config.is_half,
                        )
                    )
                    logger.info("Execute: " + cmd)
                    p = Popen(
                        cmd, shell=True, cwd=now_dir
                    )  # , shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=now_dir
                    ps.append(p)
                # 煞笔gr, popen read都非得全跑完了再一次性读取, 不用gr就正常读一句输出一句;只能额外弄出一个文本流定时读
                done = [False]
                threading.Thread(
                    target=if_done_multi,  #
                    args=(
                        done,
                        ps,
                    ),
                ).start()
            else:
                cmd = (
                    config.python_cmd
                    + ' infer/modules/train/extract/extract_f0_rmvpe_dml.py "%s/logs/%s" '
                    % (
                        now_dir,
                        exp_dir,
                    )
                )
                logger.info("Execute: " + cmd)
                p = Popen(
                    cmd, shell=True, cwd=now_dir
                )  # , shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=now_dir
                p.wait()
                done = [True]
        while 1:
            with open(
                "%s/logs/%s/extract_f0_feature.log" % (now_dir, exp_dir), "r"
            ) as f:
                yield (f.read())
            sleep(1)
            if done[0]:
                break
        with open("%s/logs/%s/extract_f0_feature.log" % (now_dir, exp_dir), "r") as f:
            log = f.read()
        logger.info(log)
        yield log
    # 对不同part分别开多进程
    """
    n_part=int(sys.argv[1])
    i_part=int(sys.argv[2])
    i_gpu=sys.argv[3]
    exp_dir=sys.argv[4]
    os.environ["CUDA_VISIBLE_DEVICES"]=str(i_gpu)
    """
    leng = len(gpus)
    ps = []
    for idx, n_g in enumerate(gpus):
        cmd = (
            '"%s" infer/modules/train/extract_feature_print.py %s %s %s %s "%s/logs/%s" %s %s'
            % (
                config.python_cmd,
                config.device,
                leng,
                idx,
                n_g,
                now_dir,
                exp_dir,
                version19,
                config.is_half,
            )
        )
        logger.info("Execute: " + cmd)
        p = Popen(
            cmd, shell=True, cwd=now_dir
        )  # , shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=now_dir
        ps.append(p)
    # 煞笔gr, popen read都非得全跑完了再一次性读取, 不用gr就正常读一句输出一句;只能额外弄出一个文本流定时读
    done = [False]
    threading.Thread(
        target=if_done_multi,
        args=(
            done,
            ps,
        ),
    ).start()
    while 1:
        with open("%s/logs/%s/extract_f0_feature.log" % (now_dir, exp_dir), "r") as f:
            yield (f.read())
        sleep(1)
        if done[0]:
            break
    with open("%s/logs/%s/extract_f0_feature.log" % (now_dir, exp_dir), "r") as f:
        log = f.read()
    logger.info(log)
    yield log

def arg_parse():
    parser = argparse.ArgumentParser(description="Process GPU configurations and settings for feature extraction.")
    parser.add_argument('--gpus', type=str, default="0", help='GPU devices to use, separated by "-"')
    parser.add_argument('--n_p', type=int, default=8, help='Number of processes to use')
    parser.add_argument('--f0method', type=str, default="rmvpe_gpu", help='Method for F0 extraction')
    parser.add_argument('--if_f0', type=bool, default=True, help='Boolean to determine if F0 extraction is needed')
    parser.add_argument('--exp_dir', type=str, default="../../data/user1/output/trained_model", help='Directory for experiment outputs')
    parser.add_argument('--version19', type=str, default="v2", help='Version of the model to use')
    parser.add_argument('--gpus_rmvpe', type=str, default="0-0", help='Specific GPUs for rmvpe method')
    return parser.parse_args()

def main():
    args = arg_parse()
    for log in extract_f0_feature(args.gpus, args.n_p, args.f0method, args.if_f0, args.exp_dir, args.version19, args.gpus_rmvpe):
        print(log)

if __name__ == "__main__":
    main()

이와 같은 방식으로 “모델 훈련” 과정과 “특징 인덱스 훈련” 과정도 분석을 하고 새로운 파일을 만들었다. 여기서는 코드가 너무 긴 관계로 적지 않겠다.