참고: https://docs.openvino.ai/2024/notebooks/freevc-voice-conversion-with-output.html
High-Quality Text-Free One-Shot Voice Conversion with FreeVC and OpenVINO™ — OpenVINO™ documentation
High-Quality Text-Free One-Shot Voice Conversion with FreeVC and OpenVINO™ This Jupyter notebook can be launched after a local installation only. FreeVC allows alter the voice of a source speaker to a target style, while keeping the linguistic content un
docs.openvino.ai
이번 글에서는 AI로 목소리 변환하는 과정을 보여준다. 사용할 모델의 이름은 FreeVC이고 자세한 내용은 여기를 참고하시길...
이 모델을 사용하는데 있어서 고려해야 할 점은 커맨드창에서 실행된다는 것과 GPU를 사용해야 한다는 것이다. (NVIDA것만?)그래서 여기서는 이것을 OpenVINO를 사용해 변경하여 이런 사항들을 보완해 보았다.
아래와 같은 그림을 붙이고 싶진 않지만 아래 코드를 조금이나마 이해하려면 어쩔수 없다. FreeVC안에는 여러가지 block들로 이루어져 있는데 이름만은 알고 가자. WavLM, Bottleneck Extractor, Speaker Encoder/Decoder, Flow....
그럼 필요한 패키지부터 설치한다. librosa, webrtcvad, gradio, torch, gdown, scipy, tqdm, torchvisionFreeVC는 아래와 같이 github에서 받아오고, notebook_utils.py와 FreeVC에서 내부적으로 사용하는 WavLM 모델도 받아온다.
이제 테스트용으로 사용할 음성파일(wav) 두개를 HF(HuggingFace)에서 받아온다.
파일들은 준비가 되었으니, import부터 시작한다.
get_cmodel()은 GPU를 사용하지 않아도 되도록 만드는 것이다. 뒤에서 다시 보자.
여기서 WavLM(), SynthesizerTrn(), load_checkpoint(), SpeakerEncoder() 등은 모두 github에서 받아온 py 파일 안에 들어있는 함수들이다.
중요한 것은 아래와 같이 이런 요소들을 모아서 시스템을 위의 그림처럼 구성한다는 것이다.
위의 block diagram에 맞춰보면 Prior Encoder 입력 y = wav_src, speaker encoder 출력 g = g_tgt, Prior Encoder 전체는 net_g, 시스템의 출력 ^y = tgt_audio로 생각할 수 있다.
일단 결과물을 들어보자.
- 원본
- 추론 결과
코드에서 보듯이 파일이 두가지가 있다. wav1, wav2라고 하면 결과물은 wav2의 목소리로 wav1의 스크립트를 읽고, 마찬가지로 wav1의 목소리로 wav2의 스크립트를 읽는 것이다. 여기서는 wav1의 길이가 짧아서 wav2만 첨부했다.
그럼 이제부터 본론인 OpenVINO로 변환하는 과정을 살펴보자.
앞에 예제에서도 봤듯이 보통 torch 모델은 convert_model()에 의해서 바로 OpenVINO IR 모델로 변환이 가능하지만, 여기서는 WavLM 모델변환시 호환성을 이유로 Wrapper를 이용하고 있다.
이렇게 변환된 모델(convert_model())은 동작할 device를 정하고 compile_model()을 이용하면 바로 사용가능하다.
이제 Speaker Encoder를 변환할 차례이다. Wrapper를 사용하지 않기에 간단하다.
그리고 나머지 net_g를 변환해 보자.
위에서 추론할 때와 같이 각 블럭의 입출력을 지정해주는 함수를 만들고, 추론해 보자. 참고로 여기서 사용한 embed_utterance()는 아래 코드 전문을 붙여놓겠다.
역시 추론 결과를 보자.
위의 추론 결과와 동일한 것 같다. 막귀이긴 하지만...
아래는 오디오 샘플링을 어떻게 하고 어떻게 나누고 등등의 오디오 전문적인 내용이긴 하다.
from speaker_encoder.hparams import sampling_rate, mel_window_step, partials_n_frames
from speaker_encoder import audio
def compute_partial_slices(n_samples: int, rate, min_coverage):
"""
Computes where to split an utterance waveform and its corresponding mel spectrogram to
obtain partial utterances of <partials_n_frames> each. Both the waveform and the
mel spectrogram slices are returned, so as to make each partial utterance waveform
correspond to its spectrogram.
The returned ranges may be indexing further than the length of the waveform. It is
recommended that you pad the waveform with zeros up to wav_slices[-1].stop.
:param n_samples: the number of samples in the waveform
:param rate: how many partial utterances should occur per second. Partial utterances must
cover the span of the entire utterance, thus the rate should not be lower than the inverse
of the duration of a partial utterance. By default, partial utterances are 1.6s long and
the minimum rate is thus 0.625.
:param min_coverage: when reaching the last partial utterance, it may or may not have
enough frames. If at least <min_pad_coverage> of <partials_n_frames> are present,
then the last partial utterance will be considered by zero-padding the audio. Otherwise,
it will be discarded. If there aren't enough frames for one partial utterance,
this parameter is ignored so that the function always returns at least one slice.
:return: the waveform slices and mel spectrogram slices as lists of array slices. Index
respectively the waveform and the mel spectrogram with these slices to obtain the partial
utterances.
"""
assert 0 < min_coverage <= 1
# Compute how many frames separate two partial utterances
samples_per_frame = int((sampling_rate * mel_window_step / 1000))
n_frames = int(np.ceil((n_samples + 1) / samples_per_frame))
frame_step = int(np.round((sampling_rate / rate) / samples_per_frame))
assert 0 < frame_step, "The rate is too high"
assert frame_step <= partials_n_frames, "The rate is too low, it should be %f at least" % (sampling_rate / (samples_per_frame * partials_n_frames))
# Compute the slices
wav_slices, mel_slices = [], []
steps = max(1, n_frames - partials_n_frames + frame_step + 1)
for i in range(0, steps, frame_step):
mel_range = np.array([i, i + partials_n_frames])
wav_range = mel_range * samples_per_frame
mel_slices.append(slice(*mel_range))
wav_slices.append(slice(*wav_range))
# Evaluate whether extra padding is warranted or not
last_wav_range = wav_slices[-1]
coverage = (n_samples - last_wav_range.start) / (last_wav_range.stop - last_wav_range.start)
if coverage < min_coverage and len(mel_slices) > 1:
mel_slices = mel_slices[:-1]
wav_slices = wav_slices[:-1]
return wav_slices, mel_slices
def embed_utterance(
wav: np.ndarray,
smodel: ov.CompiledModel,
return_partials=False,
rate=1.3,
min_coverage=0.75,
):
"""
Computes an embedding for a single utterance. The utterance is divided in partial
utterances and an embedding is computed for each. The complete utterance embedding is the
L2-normed average embedding of the partial utterances.
:param wav: a preprocessed utterance waveform as a numpy array of float32
:param smodel: compiled speaker encoder model.
:param return_partials: if True, the partial embeddings will also be returned along with
the wav slices corresponding to each partial utterance.
:param rate: how many partial utterances should occur per second. Partial utterances must
cover the span of the entire utterance, thus the rate should not be lower than the inverse
of the duration of a partial utterance. By default, partial utterances are 1.6s long and
the minimum rate is thus 0.625.
:param min_coverage: when reaching the last partial utterance, it may or may not have
enough frames. If at least <min_pad_coverage> of <partials_n_frames> are present,
then the last partial utterance will be considered by zero-padding the audio. Otherwise,
it will be discarded. If there aren't enough frames for one partial utterance,
this parameter is ignored so that the function always returns at least one slice.
:return: the embedding as a numpy array of float32 of shape (model_embedding_size,). If
<return_partials> is True, the partial utterances as a numpy array of float32 of shape
(n_partials, model_embedding_size) and the wav partials as a list of slices will also be
returned.
"""
# Compute where to split the utterance into partials and pad the waveform with zeros if
# the partial utterances cover a larger range.
wav_slices, mel_slices = compute_partial_slices(len(wav), rate, min_coverage)
max_wave_length = wav_slices[-1].stop
if max_wave_length >= len(wav):
wav = np.pad(wav, (0, max_wave_length - len(wav)), "constant")
# Split the utterance into partials and forward them through the model
mel = audio.wav_to_mel_spectrogram(wav)
mels = np.array([mel[s] for s in mel_slices])
with torch.no_grad():
mels = torch.from_numpy(mels).to(torch.device("cpu"))
output_layer = smodel.output(0)
partial_embeds = smodel(mels)[output_layer]
# Compute the utterance embedding from the partial embeddings
raw_embed = np.mean(partial_embeds, axis=0)
embed = raw_embed / np.linalg.norm(raw_embed, 2)
if return_partials:
return embed, partial_embeds, wav_slices
return embed
'AI > OpenVINO' 카테고리의 다른 글
3-11. Text-to-speech generation using Bark and OpenVINO (4) | 2024.07.24 |
---|---|
3-10. Named entity recognition(NER) with OpenVINO™ (2) | 2024.07.22 |
3-9. Convert a TensorFlow Object Detection Model to OpenVINO™ (0) | 2024.07.19 |
3-8. Background removal with RMBG v1.4 and OpenVINO (1) | 2024.07.18 |
3-6. Hello Image Segmentation (2) | 2024.07.16 |