リフュージョンと OpenVINO を使用したテキストからの音楽生成

この Jupyter ノートブックは、ローカルへのインストール後にのみ起動できます。

GitHub

リフュージョンは、任意のテキスト入力に基づいてスペクトログラム画像を生成できる潜在的なテキストから画像への拡散モデルです。これらのスペクトログラムはオーディオクリップに変換できます。一般に、拡散モデルは、画像などの対象サンプルを取得するために、ランダムなガウスノイズを段階的に除去するようにトレーニングされたマシン・ラーニング・システムです。拡散モデルは、画像データを生成するため最先端の結果を達成することが示されています。しかし、拡散モデルには、逆ノイズ除去プロセスが遅いという欠点があります。さらに、このモデルはピクセル空間で動作するため大量のメモリーを消費し、高解像度の画像を生成するには高いコストがかかります。そのため、このモデルをトレーニングし、推論に使用するのは困難です。OpenVINO は、インテルのハードウェア上でモデル推論を実行する機能を提供し、誰もが拡散モデルの素晴らしい世界への扉を開くことができます。

このチュートリアルでは、リフュージョンと OpenVINO を使用してテキストから音楽を生成するパイプラインを実行する方法について説明します。Diffusers ライブラリーの事前トレーニング済みモデルを使用します。ユーザー・エクスペリエンスを簡素化するために、Hugging Face の Optimum Intel ライブラリーを使用してモデルを OpenVINO™ IR 形式に変換します。

このチュートリアルは次のステップで構成されます。

  • 前提条件のインストール

  • OpenVINO と Hugging Face Optimum の統合を使用して、パブリックソースからモデルをダウンロードして変換します。

  • テキストから音楽への推論パイプラインを作成

  • 推論パイプラインの実行

リフュージョンについて

リフュージョンは Stable Diffusion v1.5 に基づいており、テキストとペアになったスペクトログラムの画像に基づいて微調整されています。オーディオ処理はモデルの下流で行われます。このモデルは、指定された入力テキストのオーディオ・スペクトログラムを生成できます。

オーディオ・スペクトログラムは、サウンドクリップの周波数内容を視覚的に表現する方法です。x 軸は時間を表し、y 軸は周波数を表します。各ピクセルの色は、その行と列で指定された周波数と時間におけるオーディオの振幅を示します。オーディオ・スペクトログラムは、サウンドクリップの周波数内容を視覚的に表現する方法です。x 軸は時間を表し、y 軸は周波数を表します。各ピクセルの色は、その行と列で指定された周波数と時間におけるオーディオの振幅を示します。

../_images/241-riffusion-text-to-music-with-output_14_0.png

*画像出典

スペクトログラムは、短時間フーリエ変換 (STFT) を使用してオーディオから計算できます。STFT は、オーディオをさまざまな振幅と位相の正弦波の組み合わせとして近似します。

STFT は可逆であるため、スペクトログラムから元のオーディオを再構築できます。このアイデアは、オーディオ生成にリフュージョンを使用する新しいアプローチです。

必要条件

%pip install -q --extra-index-url https://download.pytorch.org/whl/cpu torch torchaudio "diffusers>=0.16.1" "transformers>=4.33.0"
%pip install -q "git+https://github.com/huggingface/optimum-intel.git" onnx "gradio>=3.34.0" "openvino>=2023.1.0"

Optimum Intel における Stable Diffusion パイプライン

リフュージョン・モデルのアーキテクチャーは Stable Diffusion と同じなので、テキストから画像への生成に Stable Diffusion パイプラインで使用できます。Optimum Intel を使用すると、Hugging Face ハブから最適化されたモデルをロードし、API を書き換えることなく OpenVINO ランタイムで推論を実行するパイプラインを作成できます。Stable Diffusion モデルを OpenVINO 形式にエクスポートすると、パイプラインへの推論中に結合された 4 つのモデルで構成される 3 つのコンポーネントに分解されます。

  • テキスト・エンコーダー

  • U-NET

  • VAE エンコーダー

  • VAE デコーダー

Stable Diffusion パイプラインの詳細については、stable-diffusion ノートブックを参照してください。

Optimum Intel で Stable Diffusion モデルを実行するには、推論パイプラインを表す optimum.intel.OVStableDiffusionPipeline クラスを使用する必要があります。 OVStableDiffusionPipeline from_pretrained メソッドによって初期化されます。export=True パラメーターを使用して、PyTorch からのオンザフライ変換モデルをサポートします。変換されたモデルは、次回の実行のために save_pretrained メソッドを使用してディスクに保存できます。

from pathlib import Path

MODEL_ID = "riffusion/riffusion-model-v1"
MODEL_DIR = Path("riffusion_pipeline")

OpenVINO を使用して推論を実行するためにドロップダウン・リストからデバイスを選択します

import ipywidgets as widgets
from openvino.runtime import Core

core = Core()

device = widgets.Dropdown(
    options=core.available_devices + ["AUTO"],
    value='AUTO',
    description='Device:',
    disabled=False,
)

device
Dropdown(description='Device:', index=2, options=('CPU', 'GPU', 'AUTO'), value='AUTO')
from optimum.intel.openvino import OVStableDiffusionPipeline

DEVICE = device.value

if not MODEL_DIR.exists():
    pipe = OVStableDiffusionPipeline.from_pretrained(MODEL_ID, export=True, device=DEVICE, compile=False)
    pipe.half()
    pipe.save_pretrained(MODEL_DIR)
else:
    pipe = OVStableDiffusionPipeline.from_pretrained(MODEL_DIR, device=DEVICE, compile=False)
INFO:nncf:NNCF initialized successfully. Supported frameworks detected: torch, tensorflow, onnx, openvino
No CUDA runtime is found, using CUDA_HOME='/usr/local/cuda'
2023-09-19 18:21:08.176653: I tensorflow/core/util/port.cc:110] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable TF_ENABLE_ONEDNN_OPTS=0.
2023-09-19 18:21:08.217600: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: AVX2 AVX512F AVX512_VNNI FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
2023-09-19 18:21:08.865600: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT
/home/ea/work/ov_venv/lib/python3.8/site-packages/transformers/deepspeed.py:23: FutureWarning: transformers.deepspeed module is deprecated and will be removed in a future version. Please import deepspeed modules directly from transformers.integrations
  warnings.warn(

スペクトログラム画像からオーディオを再構築する後処理を準備

リフュージョン・モデルは、オーディオを再構築するために使用できるオーディオ・スペクトログラム画像を生成します。ただし、モデルからのスペクトログラム画像には正弦波の振幅のみが含まれ、位相は含まれません。位相は混沌としていて学習が難しいためです。代わりに、オーディオクリップを再構築するときに、Griffin-Lim アルゴリズムを使用して位相を近似することができます。Griffin-Lim アルゴリズム (GLA) は、短時間フーリエ変換 (STFT) の冗長性に基づいた位相再構成法です。これは、2 つの投影を反復することによってスペクトログラムの一貫性を促進します。スペクトログラムは、STFT の冗長性によるビン間の依存性が保持されている場合に一貫性があると言われます。GLA は一貫性のみに基づいており、ターゲット信号に関する事前の知識は考慮されません。

生成されたスペクトログラムの周波数ビンはメルスケールを使用します。メルスケールは、リスナーが互いに等しい距離にあると判断するピッチの知覚スケールです。

以下のコードは、Griffin-Lim アルゴリズムを使用してスペクトログラム画像から WAV オーディオクリップを再構築するプロセスを定義します。

import io
from typing import Tuple

import numpy as np
from PIL import Image
from scipy.io import wavfile
import torch
import torchaudio


def wav_bytes_from_spectrogram_image(image: Image.Image) -> Tuple[io.BytesIO, float]:
    """
    Reconstruct a WAV audio clip from a spectrogram image. Also returns the duration in seconds.

    Parameters:
      image (Image.Image): generated spectrogram image
    Returns:
      wav_bytes (io.BytesIO): audio signal encoded in wav bytes
      duration_s (float): duration in seconds
    """

    max_volume = 50
    power_for_image = 0.25
    Sxx = spectrogram_from_image(image, max_volume=max_volume, power_for_image=power_for_image)

    sample_rate = 44100  # [Hz]
    clip_duration_ms = 5000  # [ms]

    bins_per_image = 512
    n_mels = 512

    # FFT parameters
    window_duration_ms = 100  # [ms]
    padded_duration_ms = 400  # [ms]
    step_size_ms = 10  # [ms]

    # Derived parameters
    num_samples = int(image.width / float(bins_per_image) * clip_duration_ms) * sample_rate
    n_fft = int(padded_duration_ms / 1000.0 * sample_rate)
    hop_length = int(step_size_ms / 1000.0 * sample_rate)
    win_length = int(window_duration_ms / 1000.0 * sample_rate)

    samples = waveform_from_spectrogram(
        Sxx=Sxx,
        n_fft=n_fft,
        hop_length=hop_length,
        win_length=win_length,
        num_samples=num_samples,
        sample_rate=sample_rate,
        mel_scale=True,
        n_mels=n_mels,
        num_griffin_lim_iters=32,
    )

    wav_bytes = io.BytesIO()
    wavfile.write(wav_bytes, sample_rate, samples.astype(np.int16))
    wav_bytes.seek(0)

    duration_s = float(len(samples)) / sample_rate

    return wav_bytes, duration_s


def spectrogram_from_image(
    image: Image.Image, max_volume: float = 50, power_for_image: float = 0.25
) -> np.ndarray:
    """
    Compute a spectrogram magnitude array from a spectrogram image.

    Parameters:
      image (image.Image): input image
      max_volume (float, *optional*, 50): max volume for spectrogram magnitude
      power_for_image (float, *optional*, 0.25): power for reversing power curve
    """
    # Convert to a numpy array of floats
    data = np.array(image).astype(np.float32)

    # Flip Y take a single channel
    data = data[::-1, :, 0]

    # Invert
    data = 255 - data

    # Rescale to max volume
    data = data * max_volume / 255

    # Reverse the power curve
    data = np.power(data, 1 / power_for_image)

    return data


def waveform_from_spectrogram(
    Sxx: np.ndarray,
    n_fft: int,
    hop_length: int,
    win_length: int,
    num_samples: int,
    sample_rate: int,
    mel_scale: bool = True,
    n_mels: int = 512,
    num_griffin_lim_iters: int = 32,
    device: str = "cpu",
) -> np.ndarray:
    """
    Reconstruct a waveform from a spectrogram.
    This is an approximate waveform, using the Griffin-Lim algorithm
    to approximate the phase.
    """
    Sxx_torch = torch.from_numpy(Sxx).to(device)

    if mel_scale:
        mel_inv_scaler = torchaudio.transforms.InverseMelScale(
            n_mels=n_mels,
            sample_rate=sample_rate,
            f_min=0,
            f_max=10000,
            n_stft=n_fft // 2 + 1,
            norm=None,
            mel_scale="htk",
        ).to(device)

        Sxx_torch = mel_inv_scaler(Sxx_torch)

    griffin_lim = torchaudio.transforms.GriffinLim(
        n_fft=n_fft,
        win_length=win_length,
        hop_length=hop_length,
        power=1.0,
        n_iter=num_griffin_lim_iters,
    ).to(device)

    waveform = griffin_lim(Sxx_torch).cpu().numpy()

    return waveform

推論パイプラインの実行

下の図は、パイプラインのワークフローを簡単に説明したものです。

riffusion_pipeline.png

riffusion_pipeline.png

ご覧のとおり、これは、生成されたスペクトログラムをオーディオ信号に変換する後処理ステップを備えた、Stable Diffusion テキストから画像への生成と非常によく似ています。まず、OVStableDiffusionPipeline は入力テキストプロンプトを受け取ります。これはトークン化され、Frozen CLIP テキスト・エンコーダーを使用して埋め込み空間に変換され、ランダム・ジェネレーターを使用して初期の潜在スペクトログラム表現を生成します。次に、U-Net はテキスト埋め込みに基づいてランダム潜在スペクトログラム画像表現を繰り返しノイズ除去します。U-Net の出力はノイズ残差であり、スケジューラー・アルゴリズムを介してノイズ除去された潜在画像表現を計算するために使用されます。ノイズ除去プロセスは、指定された回数 (デフォルトでは 50 回) 繰り返され、段階的に潜在画像表現の改善が図られます。完了すると、潜在画像表現は変分オートエンコーダーのデコーダー部によってデコードされます。生成されたスペクトログラム画像はスペクトログラムの振幅範囲に変換され、逆メルスケールが適用されて、メル周波数領域から通常の周波数領域の STFT が推定されます。最後に、Griffin-Lim アルゴリズムによってオーディオ信号の位相が近似され、再構築されたオーディオが得られます。

pipe.reshape(batch_size=1, height=512, width=512, num_images_per_prompt=1)
pipe.compile()

def generate(prompt:str, negative_prompt:str = "") -> Tuple[Image.Image, str]:
    """
    function for generation audio from text prompt

    Parameters:
      prompt (str): input prompt for generation.
      negative_prompt (str): negative prompt for generation, contains undesired concepts for generation, which should be avoided. Can be empty.
    Returns:
      spec (Image.Image) - generated spectrogram image
    """
    spec = pipe(prompt, negative_prompt=negative_prompt, num_inference_steps=20).images[0]
    wav = wav_bytes_from_spectrogram_image(spec)
    with open("output.wav", "wb") as f:
        f.write(wav[0].getbuffer())
    return spec, "output.wav"
Compiling the vae_decoder...
Compiling the unet...
Compiling the vae_encoder...
Compiling the text_encoder...

ここで、私たちの生成を試すことができます。関数 generate はテキスト入力を受け入れ、生成されたスペクトログラムと生成されたオーディオへのパスを返します。オプションで、否定プロンプトも受け入れます。否定プロンプトは、生成に望ましくない概念を宣言します。例えば、器楽音楽を生成したい場合、オーディオにボーカルがあると望ましくない効果になるため、この場合はボーカルを否定プロンプトとして扱うことができます。肯定プロンプトと否定プロンプトは同等です。どちらか一方だけを常に使用することも、もう一方なしで使用することもできます。動作の仕組みの詳細については、この記事を参照してください。

spectrogram, wav_path = generate("Techno beat")
height was set to 256 but the static model will output images of height 512.To fix the height, please reshape your model accordingly using the .reshape() method.
width was set to 256 but the static model will output images of width 512.To fix the width, please reshape your model accordingly using the .reshape() method.
/home/ea/work/ov_venv/lib/python3.8/site-packages/optimum/intel/openvino/modeling_diffusion.py:559: FutureWarning: shared_memory is deprecated and will be removed in 2024.0. Value of shared_memory is going to override share_inputs value. Please use only share_inputs explicitly.
  outputs = self.request(inputs, shared_memory=True)
0%|          | 0/21 [00:00<?, ?it/s]
/home/ea/work/ov_venv/lib/python3.8/site-packages/optimum/intel/openvino/modeling_diffusion.py:590: FutureWarning: shared_memory is deprecated and will be removed in 2024.0. Value of shared_memory is going to override share_inputs value. Please use only share_inputs explicitly.
  outputs = self.request(inputs, shared_memory=True)
/home/ea/work/ov_venv/lib/python3.8/site-packages/optimum/intel/openvino/modeling_diffusion.py:606: FutureWarning: shared_memory is deprecated and will be removed in 2024.0. Value of shared_memory is going to override share_inputs value. Please use only share_inputs explicitly.
  outputs = self.request(inputs, shared_memory=True)
spectrogram
../_images/241-riffusion-text-to-music-with-output_14_0.png
import IPython.display as ipd
ipd.Audio(wav_path)

インタラクティブなデモ

import gradio as gr
from openvino.runtime import Core

available_devices = Core().available_devices + ["AUTO"]

examples = [
    "acoustic folk violin jam",
    "bossa nova with distorted guitar",
    "arabic gospel vocals",
    "piano funk",
    "swing jazz trumpet",
    "jamaican dancehall vocals",
    "ibiza at 3am",
    "k-pop boy group",
    "laughing",
    "water drops"
]

def select_device(device_str:str, current_text:str = "", progress:gr.Progress = gr.Progress()):
    """
    Helper function for uploading model on the device.

    Parameters:
      device_str (str): Device name.
      current_text (str): Current content of user instruction field (used only for backup purposes, temporally replacing it on the progress bar during model loading).
      progress (gr.Progress): gradio progress tracker
    Returns:
      current_text
    """
    if device_str != pipe._device:
        pipe.clear_requests()
        pipe.to(device_str)

        for i in progress.tqdm(range(1), desc=f"Model loading on {device_str}"):
            pipe.compile()
    return current_text

with gr.Blocks() as demo:

    with gr.Column():
        gr.Markdown(
            "# Riffusion music generation with OpenVINO\n"
            " Describe a musical prompt, generate music by getting a spectrogram image and sound."
        )

        prompt_input = gr.Textbox(placeholder="", label="Musical prompt")
        negative_prompt = gr.Textbox(label="Negative prompt")
        device = gr.Dropdown(choices=available_devices, value=DEVICE, label="Device")

        send_btn = gr.Button(value="Get a new spectrogram!")
        gr.Examples(examples, prompt_input, examples_per_page=15)

    with gr.Column():
        sound_output = gr.Audio(type='filepath', label="spectrogram sound")
        spectrogram_output = gr.Image(label="spectrogram image result", height=256)

    send_btn.click(generate, inputs=[prompt_input, negative_prompt], outputs=[spectrogram_output, sound_output])
    device.change(select_device, [device, prompt_input], [prompt_input])

if __name__ == "__main__":
    try:
        demo.queue().launch(debug=False, height=800)
    except Exception:
        demo.queue().launch(debug=False, share=True, height=800)

# If you are launching remotely, specify server_name and server_port
# EXAMPLE: `demo.launch(server_name='your server name', server_port='server port in int')`
# To learn more please refer to the Gradio docs: https://gradio.app/docs/
/tmp/ipykernel_180612/330468370.py:56: GradioDeprecationWarning: The style method is deprecated. Please set these arguments in the constructor instead.
  spectrogram_output.style(height=256)
/tmp/ipykernel_180612/330468370.py:63: GradioDeprecationWarning: The enable_queue parameter has been deprecated. Please use the .queue() method instead.
  demo.launch(enable_queue=True, height=800)
Running on local URL:  http://127.0.0.1:7860

To create a public link, set share=True in launch().