DOK × OpenVoiceで音声クローンを試す

[DOK](https://www.sakura.ad.jp/koukaryoku-dok/)はコンテナー型のGPUサービスで、NVIDIA V100やH100を実行時間課金で利用できるサービスです。

[コンテナー型GPUクラウドサービス 高火力 DOK（ドック） | さくらインターネット](https://www.sakura.ad.jp/koukaryoku-dok/)

今回はこのDOKを使って、OpenVoiceにて自分の声を使った音声クローンを行ってみます。

## 結果

結果の音声です。日本語はもちろん、英語（インド風、オーストラリア風など）も生成できます。

- 日本語  
https://s3.isk01.sakurastorage.jp/mg-sd-demo/openvoice/002_jp.wav
- 英語（US）  
https://s3.isk01.sakurastorage.jp/mg-sd-demo/openvoice/6415b7f3-4b95-41bd-b0f1-8597418bcdbd_en-us.wav
- 英語（インド）  
https://s3.isk01.sakurastorage.jp/mg-sd-demo/openvoice/6415b7f3-4b95-41bd-b0f1-8597418bcdbd_en-india.wav
- 英語（オーストラリア）  
https://s3.isk01.sakurastorage.jp/mg-sd-demo/openvoice/6415b7f3-4b95-41bd-b0f1-8597418bcdbd_en-au.wav
- 英語（ブラジル）  
https://s3.isk01.sakurastorage.jp/mg-sd-demo/openvoice/6415b7f3-4b95-41bd-b0f1-8597418bcdbd_en-br.wav
- 英語（デフォルト）  
https://s3.isk01.sakurastorage.jp/mg-sd-demo/openvoice/6415b7f3-4b95-41bd-b0f1-8597418bcdbd_en-default.wav


## 参考

[myshell\-ai/OpenVoice](https://github.com/myshell-ai/OpenVoice/)にある[USAGE\.md](https://github.com/myshell-ai/OpenVoice/blob/main/docs/USAGE.md)を参考に、v2で行っています。

## とりあえず試す

学習済みのデータを使って実行してみたい方は、DOKにて新しいタスクを作成し、以下の情報を入力してください。

| 項目     | 設定                                                          |
| -------- | ------------------------------------------------------------- |
| イメージ | dok-handson.sakuracr.jp/openvoice                            |
| 環境変数 | TEXT = "Now, here is today's news. It was a nice day today."<br />LANG = EN |

TEXTは読み上げる文章です。LANGは言語を指定します。ENは英語、JAは日本語です。この場合はリファレンスの音声は指定していないので、デフォルトの[example_reference.mp3](https://github.com/myshell-ai/OpenVoice/blob/main/resources/example_reference.mp3)が利用されます。

> 自分の声など、特定の音声を指定してクローンを作りたい場合には、参考にする音声のMP3ファイルをどこかにアップロードし、そのURLを `REFERENCE` という環境変数で指定してください。
> その音声は、長い方が良いようです。3分程度、適当な文章（なるべくさまざまな単語が含まれるもの）を読み、MP3ファイルにしてください

なお、プランはv100-32gbで大丈夫そうです。試してみた限りですが、v100-32gbで2分、h100-80gbで1分10秒でした（リファレンスあり）。なお、Dockerコンテナのダウンロードや初期化には時間がかかります（10分弱）。

## コンテナイメージの作成と登録

上記タスクで利用したDockerイメージを作成する手順は以下の通りです。完成版は[goofmint/dok-openvoice](https://github.com/goofmint/openvoice)にありますので、実装時の参考にしてください。

### ベースになるリポジトリ

ベースは[myshell\-ai/OpenVoice: Instant voice cloning by MIT and MyShell\. Audio foundation model\.](https://github.com/myshell-ai/OpenVoice)を利用します。

```bash
git clone https://github.com/myshell-ai/OpenVoice.git
cd OpenVoice
```

この中に必要なファイルを作成していきます。

### checkpoint のダウンロードと解凍

[checkpoint](https://myshell-public-repo-host.s3.amazonaws.com/openvoice/checkpoints_v2_0417.zip)をダウンロードし、 `checkpoints_v2` ディレクトリに解凍します。

```bash
wget https://myshell-public-repo-host.s3.amazonaws.com/openvoice/checkpoints_v2_0417.zip
unzip checkpoints_v2_0417.zip
```

### Dockerfile の作成

ここからはDockerfileの中で行っていきます。

#### ベースイメージ

ベースは `FROM continuumio/miniconda3` です。

```Dockerfile
FROM continuumio/miniconda3
```

#### conda の設定

condaの設定を行います。

```Dockerfile
RUN conda create -n openvoice python==3.9
SHELL ["conda", "run", "-n", "openvoice", "/bin/bash", "-c"]
```

#### 必要なライブラリのインストール

OpenVoiceはffmpegを使っているので、それをインストールします。 `/opt/artifact` はDOKのアウトプット先のディレクトリです。

```Dockerfile
RUN apt-get update && \
    apt-get install -y ffmpeg && \
    mkdir /app /opt/artifact
```

#### 必要なPythonライブラリのインストール

OpenVoiceのリポジトリのファイルをすべてコピーします。そして、OpenVoice用に[MeloTTS](https://github.com/myshell-ai/MeloTTS)をインストールします。argparseやboto3はCLI実行用にインストールします。

```Dockerfile
WORKDIR /app
COPY . .
RUN pip install -e . && \
  pip install argparse && \
  pip install boto3 && \
  pip install git+https://github.com/myshell-ai/MeloTTS.git && \
  python -m unidic download
```

#### docker-entrypoint.sh の準備

Docker実行の際に呼び出す `docker-entrypoint.sh` （内容は後述）を準備します。

```Dockerfile
RUN chmod +x /app/docker-entrypoint.sh

# Dockerコンテナー起動時に実行するスクリプトを指定して実行
CMD ["/bin/bash", "/app/docker-entrypoint.sh"]
```

### Dockerfile全体

Dockerfileの全体は以下の通りです。

```Dockerfile
FROM continuumio/miniconda3

RUN conda create -n openvoice python==3.9
SHELL ["conda", "run", "-n", "openvoice", "/bin/bash", "-c"]
RUN apt-get update && \
    apt-get install -y ffmpeg && \
    mkdir /app /opt/artifact

WORKDIR /app
COPY . .
RUN pip install -e . && \
  pip install argparse && \
  pip install boto3 && \
  pip install git+https://github.com/myshell-ai/MeloTTS.git && \
  python -m unidic download

RUN chmod +x /app/docker-entrypoint.sh

# Dockerコンテナー起動時に実行するスクリプトを指定して実行
CMD ["/bin/bash", "/app/docker-entrypoint.sh"]
```

### docker-entrypoint.sh の作成

`docker-entrypoint.sh` はDockerコンテナー起動時に実行するスクリプトです。ここでは環境変数をチェックして、 `runner.py` を呼び出します。以下の内容で作成します。

```bash
#!/bin/bash

set -ue
shopt -s nullglob

export TZ=${TZ:-Asia/Tokyo}

# アウトプット先ディレクトリ（自動付与） /opt/artifact固定です
if [ -z "${SAKURA_ARTIFACT_DIR:-}" ]; then
  echo "Environment variable SAKURA_ARTIFACT_DIR is not set" >&2
  exit 1
fi

# DOKのタスクID（自動付与）
if [ -z "${SAKURA_TASK_ID:-}" ]; then
  echo "Environment variable SAKURA_TASK_ID is not set" >&2
  exit 1
fi

# 読み上げるテキスト（環境変数で指定）
if [ -z "${TEXT:-}" ]; then
  echo "Environment variable PROMPT is not set" >&2
  exit 1
fi

# 言語
if [ -z "${LANG:-}" ]; then
  echo "Environment variable LANG is not set" >&2
  exit 1
fi

# リファレンスの音声（オプション）
if [ -z "${REFERENCE:-}" ]; then
    REFERENCE="resources/example_reference.mp3"
else
    wget "$REFERENCE" -O /tmp/reference.mp3
    REFERENCE="/tmp/reference.mp3"
fi

# S3_はすべてboto3用の環境変数です
cd /app
conda run -n openvoice python3 runner.py \
	  --id="${SAKURA_TASK_ID}" \
	  --output="${SAKURA_ARTIFACT_DIR}" \
	  --text="${TEXT}" \
	  --lang="${LANG}" \
	  --reference="${REFERENCE}" \
	  --s3-bucket="${S3_BUCKET:-}" \
	  --s3-endpoint="${S3_ENDPOINT:-}" \
	  --s3-secret="${S3_SECRET:-}" \
	  --s3-token="${S3_TOKEN:-}"
```

### runner.py の作成

runner.py は実際に処理を行うスクリプトです。まず必要なライブラリをインポートします。

```python
import os
import torch
from openvoice import se_extractor
from openvoice.api import ToneColorConverter
from melo.api import TTS
import argparse
import nltk
import boto3

nltk.download('averaged_perceptron_tagger_eng')
```

#### パラメータの取得

`docker-entrypoint.sh` から渡されたパラメータを取得します。

```python
arg_parser = argparse.ArgumentParser()

arg_parser.add_argument(
    '--output',
    default='/opt/artifact',
    help='出力先ディレクトリを指定します。',
)
arg_parser.add_argument(
    '--text',
    default='',
    help='読み上げる文章',
)
arg_parser.add_argument(
    '--id',
    default='',
    help='タスクIDを指定します。',
)
arg_parser.add_argument(
    '--lang',
    default='JP',
    help='読み上げる言語を指定',
)
arg_parser.add_argument(
    '--reference',
    default='resources/example_reference.mp3',
    help='リファレンスの音声',
)

arg_parser.add_argument('--s3-bucket', help='S3のバケットを指定します。')
arg_parser.add_argument('--s3-endpoint', help='S3互換エンドポイントのURLを指定します。')
arg_parser.add_argument('--s3-secret', help='S3のシークレットアクセスキーを指定します。')
arg_parser.add_argument('--s3-token', help='S3のアクセスキーIDを指定します。')

args = arg_parser.parse_args()
```

### オブジェクトストレージ用のオブジェクトを準備

S3_ではじまる環境変数があれば、それを使ってS3オブジェクトを作成します。

```python
s3 = None
if args.s3_token and args.s3_secret and args.s3_bucket:
    # S3クライアントの作成
    s3 = boto3.client(
        's3',
        endpoint_url=args.s3_endpoint if args.s3_endpoint else None,
        aws_access_key_id=args.s3_token,
        aws_secret_access_key=args.s3_secret)
```

### OpenVoiceの設定

[OpenVoice/demo\_part3\.ipynb at main · myshell\-ai/OpenVoice](https://github.com/myshell-ai/OpenVoice/blob/main/demo_part3.ipynb)の内容に沿って、OpenVoiceを準備します。

```python
ckpt_converter = 'checkpoints_v2/converter'
device = "cuda:0" if torch.cuda.is_available() else "cpu"
output_dir = args.output

tone_color_converter = ToneColorConverter(f'{ckpt_converter}/config.json', device=device)
tone_color_converter.load_ckpt(f'{ckpt_converter}/checkpoint.pth')

os.makedirs(output_dir, exist_ok=True)

reference_speaker = args.reference
target_se, audio_name = se_extractor.get_se(reference_speaker, tone_color_converter, vad=True)

from melo.api import TTS

src_path = f'{output_dir}/tmp.wav'

speed = 1.0

model = TTS(language=args.lang, device=device)
speaker_ids = model.hps.data.spk2id
```

### 音声の生成とアップロード

音声を生成し、アップロードします。たとえば `EN` を指定した場合、インド風英語やオーストラリア風英語など複数のキーが `speaker_ids` に入ってきます。

```python
for speaker_key in speaker_ids.keys():
    speaker_id = speaker_ids[speaker_key]
    speaker_key = speaker_key.lower().replace('_', '-')
    source_se = torch.load(f'checkpoints_v2/base_speakers/ses/{speaker_key}.pth', map_location=device)
    model.tts_to_file(args.text, speaker_id, src_path, speed=speed)
    save_path = f'{args.output}/{args.id}_{speaker_key}.wav'
    # Run the tone color converter
    encode_message = "@MyShell"
    tone_color_converter.convert(
        audio_src_path=src_path, 
        src_se=source_se, 
        tgt_se=target_se, 
        output_path=save_path,
        message=encode_message)
    # オブジェクトストレージにアップロード
    if s3 is not None:
        s3.upload_file(
            Filename=save_path,
            Bucket=args.s3_bucket,
            Key=os.path.basename(save_path))
```

### 全体の処理

`runner.py` の全体の処理は以下の通りです。

```python
import os
import torch
from openvoice import se_extractor
from openvoice.api import ToneColorConverter
from melo.api import TTS
import argparse
import nltk
import boto3

nltk.download('averaged_perceptron_tagger_eng')

arg_parser = argparse.ArgumentParser()

arg_parser.add_argument(
    '--output',
    default='/opt/artifact',
    help='出力先ディレクトリを指定します。',
)
arg_parser.add_argument(
    '--text',
    default='',
    help='読み上げる文章',
)
arg_parser.add_argument(
    '--id',
    default='',
    help='タスクIDを指定します。',
)
arg_parser.add_argument(
    '--lang',
    default='JP',
    help='読み上げる言語を指定',
)
arg_parser.add_argument(
    '--reference',
    default='resources/example_reference.mp3',
    help='リファレンスの音声',
)

arg_parser.add_argument('--s3-bucket', help='S3のバケットを指定します。')
arg_parser.add_argument('--s3-endpoint', help='S3互換エンドポイントのURLを指定します。')
arg_parser.add_argument('--s3-secret', help='S3のシークレットアクセスキーを指定します。')
arg_parser.add_argument('--s3-token', help='S3のアクセスキーIDを指定します。')

args = arg_parser.parse_args()

s3 = None
if args.s3_token and args.s3_secret and args.s3_bucket:
    # S3クライアントの作成
    s3 = boto3.client(
        's3',
        endpoint_url=args.s3_endpoint if args.s3_endpoint else None,
        aws_access_key_id=args.s3_token,
        aws_secret_access_key=args.s3_secret)

ckpt_converter = 'checkpoints_v2/converter'
device = "cuda:0" if torch.cuda.is_available() else "cpu"
output_dir = args.output

tone_color_converter = ToneColorConverter(f'{ckpt_converter}/config.json', device=device)
tone_color_converter.load_ckpt(f'{ckpt_converter}/checkpoint.pth')

os.makedirs(output_dir, exist_ok=True)

reference_speaker = args.reference
target_se, audio_name = se_extractor.get_se(reference_speaker, tone_color_converter, vad=True)


src_path = f'{output_dir}/tmp.wav'

speed = 1.0

model = TTS(language=args.lang, device=device)
speaker_ids = model.hps.data.spk2id

for speaker_key in speaker_ids.keys():
    speaker_id = speaker_ids[speaker_key]
    speaker_key = speaker_key.lower().replace('_', '-')
    source_se = torch.load(f'checkpoints_v2/base_speakers/ses/{speaker_key}.pth', map_location=device)
    model.tts_to_file(args.text, speaker_id, src_path, speed=speed)
    save_path = f'{args.output}/{args.id}_{speaker_key}.wav'
    # Run the tone color converter
    encode_message = "@MyShell"
    tone_color_converter.convert(
        audio_src_path=src_path, 
        src_se=source_se, 
        tgt_se=target_se, 
        output_path=save_path,
        message=encode_message)
    if s3 is not None:
        s3.upload_file(
            Filename=save_path,
            Bucket=args.s3_bucket,
            Key=os.path.basename(save_path))
```

### Dockerイメージのビルド

上記の内容で、Dockerイメージをビルドします。Linux環境などで行います。

#### コンテナレジストリの用意

Dockerイメージを登録するコンテナレジストリを作成します。さくらのクラウドではLAB機能で、コンテナレジストリを提供しています。さくらのクラウドにログインしたら `さくらのクラウド` を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/e247a30c-bbc7-fb7e-136d-71ee2a795533.png)

左側のメニューの `LAB` の中にある `コンテナレジストリ` を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/25d041f2-9039-fec3-77c6-88edf5afa936.png)

`追加` を押して、コンテナレジストリを作成します。最低限、以下の入力が必要です。

| 項目                 | 設定                                                                      |
| -------------------- | ------------------------------------------------------------------------- |
| 名前                 | 分かりやすい、任意の名前を入力してください                                |
| コンテナレジストリ名 | ドメイン名に使われます。以下では、 `EXAMPLE.sakuracr.jp` として説明します |
| 公開設定             | Pullのみとします                                                          |

#### ユーザーの作成

コンテナレジストリを作成したら、作成したコンテナレジストリを一覧でダブルクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/6425f493-0ce1-db62-f322-64a51867ce58.png)

詳細表示にて、ユーザータブをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/79bdc51c-ea66-8d8f-c801-8f54346cf960.png)

追加ボタンを押し、ユーザーを作成します。 `YOUR_USER_NAME` と `PASSWORD` は任意のものを指定してください。

| 項目           | 設定           |
| -------------- | -------------- |
| ユーザー名     | YOUR_USER_NAME |
| パスワード     | YOUR_PASSWORD  |
| ユーザ権限設定 | All            |

#### Dockerイメージのビルド

DockerイメージはLinuxで行います。今回はUbuntu 24.04を使っています。Dockerが使える環境であれば、Windows + WSL2でも問題ありません。macOSの場合、アーキテクチャが異なるので動かせないかも知れません（未検証です）。

`EXAMPLE.sakuracr.jp` の部分は、作成したコンテナレジストリのドメイン名に置き換えてください。また、 `openvoice` は任意の名前で大丈夫です（以下はその名称で読み替えてください）。

```bash
sudo docker build -t EXAMPLE.sakuracr.jp/openvoice:latest .
```

#### コンテナレジストリへのログイン

作成したコンテナレジストリにログインします。ログインIDとパスワードが求められるので、作成したものを入力してください。

```bash
sudo docker login EXAMPLE.sakuracr.jp
```

#### イメージのプッシュ

作成したイメージをコンテナレジストリにプッシュします。イメージサイズが大きいので、数十分かかります。

```bash
sudo docker push EXAMPLE.sakuracr.jp/openvoice:latest
```

### タスクを作成する

後は最初と同じようにDOKでタスクを作成、実行します。

| 項目     | 設定                                                          |
| -------- | ------------------------------------------------------------- |
| イメージ | dok-handson.sakuracr.jp/openvoice                            |
| 環境変数 | TEXT = "Now, here is today's news. It was a nice day today."<br />LANG = EN |

![FireShot Capture 393 - 高火力 DOK - タスク › 新規作成 - secure.sakura.ad.jp.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/bd0adada-1eee-3349-8a36-b9b3f11a1c69.jpeg)


音声クローンを行う際には、リファレンスの音声が必要です。ボイスレコーダーのようなアプリで自分の声を録音（2〜3分程度）し、それを任意の場所にアップロードします。たとえばさくらのオブジェクトストレージを使った場合、以下のようなURLになります（パブリックリードを指定します）。

```
https://s3.isk01.sakurastorage.jp/YOUR_BACKET_NAME/YOUR_REFERENCE.mp3
```

## まとめ

今回はOpenVoiceを使って、DOK上で音声クローンを行ってみました。まずは実行できるのみ、次にDockerイメージの作成と段階的に進められるようにしています。ぜひ、試してみてください。

DOKはタスクを多数立ち上げて、後は結果を待つのみと言った使い方ができます。ぜひAI・機械学習に活用してください。

[コンテナー型GPUクラウドサービス 高火力 DOK（ドック） \| さくらインターネット](https://www.sakura.ad.jp/koukaryoku-dok/)