OpenVoiceでゼロショット音声クローニング+感情制御を試した実録

この記事のまとめ(先に読む)
OpenVoice V1 は、わずか14秒の参照音声から話者の声色を抽出し、whisperingshoutingexcitedcheerfulterrifiedangrysadfriendly の8スタイルで音声を生成できるゼロショット音声クローニングライブラリです。本記事では実際に動かしたコードと観察結果、ハマりポイントをまとめます。


OpenVoiceとは何か

OpenVoice は MyShell が開発しオープンソース公開している音声クローニングフレームワークです。大きな特徴は トーンカラー(声紋)分離スタイル制御 を独立したモジュールに分けている点です。

コンポーネント役割
BaseSpeakerTTS感情スタイル付きで音声を生成するベースモデル
ToneColorConverter任意の参照音声から声色を抽出・変換する
se_extractor参照音声から Speaker Embedding(話者埋め込みベクトル)を取得

「感情はベースモデルが担い、声色は参照音声が担う」という分業構造です。参照音声がどんな感情で話していても、出力側で感情を自由に切り替えられます。

ゼロショット(Zero-shot)とは
学習済みモデルを追加ファインチューニングなしに、初見の話者の音声へ適応させること。数秒〜数十秒の参照音声だけで動作するため、データ収集コストがほぼゼロです。


実験設定

  • 参照音声: 14秒の英語音声(単一話者、平静なトーン)
  • 合成テキスト: "This audio is generated by OpenVoice."(デフォルト文)
  • 試したスタイル: whispering / shouting / excited / cheerful / terrified / angry / sad / friendly の全8種
  • 環境: Python 3.10、PyTorch 2.x、CUDA 対応 GPU(CPU 動作も可)

参照音声はこちら。


生成結果

実際に生成した音声ファイルです。同じテキスト・同じ話者の声色でスタイルだけを切り替えています。

  • whispering

  • friendly

  • terrified

  • shouting

  • angry

  • cheerful

  • excited.

  • sad


実際に動かしたコード

--reference に参照音声を渡し、--style でスタイルを指定するだけで動くCLIスクリプトです。

python
#!/usr/bin/env python3
import argparse
import os
import sys

import torch

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if ROOT not in sys.path:
    sys.path.insert(0, ROOT)

from openvoice import se_extractor
from openvoice.api import BaseSpeakerTTS, ToneColorConverter


ENGLISH_STYLES = [
    "default",
    "whispering",
    "shouting",
    "excited",
    "cheerful",
    "terrified",
    "angry",
    "sad",
    "friendly",
]

DEFAULT_TEXTS = {
    "English": "This audio is generated by OpenVoice.",
    "Chinese": "今天天气真好,我们一起出去吃饭吧。",
}


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Run OpenVoice V1 style-controlled voice cloning."
    )
    parser.add_argument(
        "--reference",
        required=True,
        help="Reference audio path used for tone color extraction.",
    )
    parser.add_argument(
        "--text", default=None, help="Text to synthesize before tone color conversion."
    )
    parser.add_argument(
        "--language",
        default="English",
        choices=["English", "Chinese"],
        help="Base speaker language used by OpenVoice V1.",
    )
    parser.add_argument(
        "--style",
        default="default",
        help="Style preset. English supports angry/sad/friendly/etc. Chinese supports default only.",
    )
    parser.add_argument(
        "--output", default="outputs/output_v1.wav", help="Output wav path."
    )
    parser.add_argument("--speed", type=float, default=1.0, help="TTS speed.")
    parser.add_argument(
        "--device", default=None, help="Torch device, e.g. cuda:0 or cpu."
    )
    parser.add_argument(
        "--tau",
        type=float,
        default=0.3,
        help="Tone color conversion strength used by the converter.",
    )
    return parser.parse_args()


def validate_args(args: argparse.Namespace) -> None:
    if args.language == "English":
        if args.style not in ENGLISH_STYLES:
            raise SystemExit(
                f"Unsupported English style '{args.style}'. Available: {', '.join(ENGLISH_STYLES)}"
            )
    elif args.style != "default":
        raise SystemExit("Chinese in OpenVoice V1 supports only --style default.")


def main() -> None:
    args = parse_args()
    validate_args(args)

    device = args.device or ("cuda:0" if torch.cuda.is_available() else "cpu")
    text = args.text or DEFAULT_TEXTS[args.language]

    ckpt_base = (
        "checkpoints/base_speakers/EN"
        if args.language == "English"
        else "checkpoints/base_speakers/ZH"
    )
    ckpt_converter = "checkpoints/converter"

    base_speaker_tts = BaseSpeakerTTS(f"{ckpt_base}/config.json", device=device)
    base_speaker_tts.load_ckpt(f"{ckpt_base}/checkpoint.pth")

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

    # スタイルに応じた Speaker Embedding を選択
    if args.language == "English":
        se_name = "en_style_se.pth" if args.style != "default" else "en_default_se.pth"
    else:
        se_name = "zh_default_se.pth"

    source_se = torch.load(f"{ckpt_base}/{se_name}", map_location=device).to(device)
    target_se, _ = se_extractor.get_se(
        args.reference, tone_color_converter, target_dir="processed_v1", vad=True
    )

    out_dir = os.path.dirname(args.output) or "."
    os.makedirs(out_dir, exist_ok=True)
    src_path = os.path.join(out_dir, "tmp_openvoice_v1.wav")

    base_speaker_tts.tts(
        text, src_path, speaker=args.style, language=args.language, speed=args.speed
    )
    tone_color_converter.convert(
        audio_src_path=src_path,
        src_se=source_se,
        tgt_se=target_se,
        output_path=args.output,
        tau=args.tau,
        message="@MyShell",
    )
    print(args.output)


if __name__ == "__main__":
    main()

セットアップ手順

bash
# 1. リポジトリのクローン
git clone https://github.com/myshell-ai/OpenVoice.git
cd OpenVoice

# 2. 依存パッケージのインストール
pip install -e .

# 3. チェックポイントのダウンロード(公式 README を参照)
# checkpoints/base_speakers/EN/ と checkpoints/converter/ を配置

# 4. 実行例(angry スタイル)
python scripts/run_v1.py \
  --reference path/to/reference.wav \
  --style angry \
  --output outputs/angry_output.wav

全スタイルを一括生成するシェル例

bash
for style in whispering shouting excited cheerful terrified angry sad friendly; do
  python scripts/run_v1.py \
    --reference reference.wav \
    --style "$style" \
    --output "outputs/${style}.wav"
done

アーキテクチャの深掘り

OpenVoice の動作は大きく 3ステップ に分けられます。

text
[1] BaseSpeakerTTS
    テキスト → スタイル付き音声(source_se の声色)
         ↓
[2] se_extractor
    参照音声 → target_se(話者埋め込みベクトル)を抽出
         ↓
[3] ToneColorConverter
    source_se × target_se → 最終音声(参照音声の声色 × 指定スタイルの感情)

重要なのは ToneColorConverter声色変換を音素レベルではなくスペクトル空間で行っている 点です。そのため参照音声の長さや内容に依存せず、14秒程度の短い音声でも安定した埋め込みを取得できます。

VAD(Voice Activity Detection)vad=True で有効にしておくと、無音区間を自動で除去してから埋め込みを計算するため、ノイズや余白の多い参照音声でも品質が安定します。

target_dir="processed_v1" に中間ファイルが書き出されます。複数の参照音声を試す場合、同じディレクトリを使い回すとキャッシュが混在することがあるので、参照音声ごとにディレクトリを分けることを推奨します。

OpenVoice V1 の BaseSpeakerTTS は英語と中国語のみ対応しています。日本語テキストをそのまま渡しても正常に発音されません。日本語が必要な場合は OpenVoice V2 または別モデルを使用してください。


FAQ

Q. 参照音声は何秒以上必要ですか?
公式には明記されていませんが、実験上 10〜20秒程度あれば安定した埋め込みが取得できます。5秒以下では声色の再現精度が落ちる傾向があります。

Q. 参照音声に背景音楽が入っていても大丈夫ですか?
vad=True を指定すると無音区間除去は行われますが、BGMは除去されません。Demucs などで音源分離してから渡すことを推奨します。

Q. 商用利用は可能ですか?
OpenVoice は MIT ライセンスで公開されています。ただし生成音声を公開・配布する際は、元の話者の声を無断使用していないか、倫理・法的観点からの確認が別途必要です。

Q. OpenVoice V1 と V2 の使い分けは?
感情スタイルを明確に制御したい英語・中国語コンテンツなら V1、多言語対応や日本語が必要なら V2 が適しています。V2 はスタイル制御の粒度が異なるため、感情の鮮明さは V1 に軍配が上がる場面もあります。

Q. tau パラメータはどう調整すればいいですか?
tau はトーンカラー変換の強度です。小さいほど参照音声の声色に近づき、大きいほど変換が強くかかります。0.1〜0.5 の範囲で試し、声色の自然さと感情の再現バランスを取るのがおすすめです。

Q. リアルタイム生成は可能ですか?
V1 の現構成はバッチ処理向けで、ストリーミング出力には対応していません。リアルタイム性が必要な場合は Fish Speech や CosyVoice のストリーミングモードを検討してください。


まとめ

OpenVoice V1 は「わずかな参照音声+スタイル指定」だけで感情豊かな音声クローニングを実現する実用的なツールです。今回の実験では14秒の参照音声から8種の感情を再現し、cheerfulfriendlywhispering などは特に高い自然さを確認できました。

セットアップのポイントをまとめると、チェックポイントのパス管理に注意すること、参照音声は10秒以上・VAD有効で渡すこと、--tau で声色と感情強度のバランスを微調整すること、そして日本語は V1 非対応なので V2 または他モデルへ移行すること、の4点が重要です。

音声クローニング・感情TTS の用途で選択肢を探している方には、まず OpenVoice V1 から試してみることをお勧めします。

関連するブログ

この記事に近いテーマのブログをピックアップしています。