LJSpeechを使って英語のpiperの事前学習モデルを作成する

Demo

学習したモデルは以下で公開しています

huggingface.co

生成した音声は以下のようになります

youtu.be

開発環境

環境の構築

まずは学習環境の作成をしていきます。

まずはライブラリをcloneします

git clone https://github.com/rhasspy/piper.git
cd piper/src/python

python以外の環境を準備します

sudo apt-get update
sudo apt-get install -y build-essential
sudo apt-get install -y python3-dev espeak-ng

次に環境を作成します。今回は uvを使ってpythonの環境の仮想環境を作ります

cd src/python
uv venv -p 3.11
source .venv/bin/activate

必要なライブラリをインストールします。

uv pip install --upgrade pip wheel setuptools
uv pip install -e . 
uv pip install pytorch-lightning
uv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
./build_monotonic_align.sh

データセットの準備

リポジトリがあるルートフォルダと同じところで、データセットのフォルダを作ってデータセットをダウンロードしていきます

mkdir -p ~/datasets
cd ~/datasets
wget https://data.keithito.com/data/speech/LJSpeech-1.1.tar.bz2
# 解凍
tar -xjvf LJSpeech-1.1.tar.bz2

このデータセットのパスを環境パスに保存しておきます

export INPUT_DATASET_DIR="your path/datasets/LJSpeech-1.1"

前処理・ログ用のフォルダの作成

前処理済みデータと学習ログを保存するディレクトリを作成します。

export TRAINING_DATA_DIR="your path/piper_ljspeech_training"
mkdir -p $TRAINING_DATA_DIR

前処理の実行

先ほど設定した環境変数を使い データセットの前処理を行っていきます

python3 -m piper_train.preprocess \
  --language en-us \
  --input-dir ${INPUT_DATASET_DIR} \
  --output-dir ${TRAINING_DATA_DIR} \
  --dataset-format ljspeech \
  --single-speaker \
  --sample-rate 22050

だいたい1時間くらいでした(スペックによります)

完了すると以下のようなログになります

ファイルによっては途中で失敗することがあります。その際には以下のような 処理済みのファイルから未処理のデータセット一覧を作成して前処理のみを再開することができます

import csv
import os
import json
import sys
from tqdm import tqdm

def create_resume_file(original_metadata_path, preprocessed_dir_path, resume_output_path):
    """
    前処理が中断した箇所から再開するための新しいメタデータファイルを作成します。
    """
    
    # 1. 既に処理済みのファイルのリストを作成する
    processed_audio_paths = set()
    partial_dataset_jsonl = os.path.join(preprocessed_dir_path, "dataset.jsonl")
    
    if not os.path.exists(partial_dataset_jsonl):
        print(f"Warning: Partial 'dataset.jsonl' not found at {partial_dataset_jsonl}. Assuming no files were processed.")
    else:
        print(f"Reading already processed files from {partial_dataset_jsonl}...")
        with open(partial_dataset_jsonl, 'r', encoding='utf-8') as f:
            for line in f:
                try:
                    data = json.loads(line)
                    if "audio_path" in data:
                        processed_audio_paths.add(data["audio_path"])
                except json.JSONDecodeError:
                    print(f"Warning: Could not decode JSON line: {line.strip()}", file=sys.stderr)
        print(f"Found {len(processed_audio_paths)} processed files.")

    # 2. 元のメタデータを読み込み、未処理の行だけを新しいファイルに書き出す
    print(f"Reading original metadata from {original_metadata_path} to find unprocessed files...")
    unprocessed_rows = []
    
    original_dataset_dir = os.path.dirname(original_metadata_path)
    
    with open(original_metadata_path, 'r', encoding='utf-8') as f:
        reader = csv.reader(f, delimiter='|')
        all_rows = list(reader)
        for row in tqdm(all_rows, desc="Comparing metadata"):
            if not row or len(row) < 1:
                continue
            
            file_id = row[0]
            # dataset.jsonlに記録されているフルパスと一致させる
            # piper_train.preprocess は入力ディレクトリからの相対パスではなく、絶対パスを記録することがあるため、
            # os.path.join で結合してフルパスを作成します。
            expected_audio_path = os.path.join(original_dataset_dir, "wavs", f"{file_id}.wav")

            if expected_audio_path not in processed_audio_paths:
                unprocessed_rows.append(row)

    print(f"Found {len(unprocessed_rows)} unprocessed files.")
    
    # 3. 未処理の行を新しいメタデータファイルに保存
    if unprocessed_rows:
        print(f"Saving resume metadata to {resume_output_path}...")
        with open(resume_output_path, 'w', encoding='utf-8', newline='') as f:
            writer = csv.writer(f, delimiter='|')
            writer.writerows(unprocessed_rows)
        print("Resume file created successfully.")
    else:
        print("No unprocessed files found. Preprocessing may have completed or an error occurred before any processing.")

if __name__ == "__main__":
    # --- ★設定箇所★ ---
    
    # Piper学習用に準備した元のデータセットのディレクトリ
    # (`metadata.csv` と `wavs/` がある場所)
    original_dataset_dir = "/data/moe-speech-plus-ljspeech" # お客様が --input-dir で指定したパス
    
    # 前処理が中断した出力先ディレクトリ
    # (中に部分的な `dataset.jsonl` がある場所)
    preprocessed_dir = "/data/piper_moe-speech-plus_preprocessed_single" # お客様が --output-dir で指定したパス

    # --- 設定ここまで ---
    
    original_metadata = os.path.join(original_dataset_dir, "metadata.csv")
    resume_metadata = os.path.join(original_dataset_dir, "metadata_resume.csv") # 新しく作成するファイル

    create_resume_file(original_metadata, preprocessed_dir, resume_metadata)

これを実行することで、metadata_resume.csv が作成されます。

このファイルをmetadata.csvに名前を変更して再度前処理を実行します

INFO:preprocess:Single speaker dataset

INFO:preprocess:Wrote dataset config

INFO:preprocess:Processing 13100 utterance(s) with 48 worker(s)

事前学習の開始

以下のコマンドにて事前学習を行います

python -m piper_train \
  --dataset-dir ${TRAINING_DATA_DIR} \
  --accelerator 'gpu' \
  --devices 1 \
  --batch-size 24 \
  --validation-split 0.05 \
  --num-test-examples 10 \
  --max_epochs 5000 \
  --checkpoint-epochs 1 \
  --precision 32 \
  --quality medium

調べた感じマルチGPUは対応していないみたいだったので、仕方なくシングルGPUで学習を行っています

学習されたモデルは以下のパスに保存されています

piper_ljspeech_training/lightning_logs/version_X/checkpoints/

学習後のlossは以下のようになりました

モデルをonnxに変換

これを推論するためにonnxに変換します

python3 -m piper_train.export_onnx \
  /data/piper_ljspeech_training/lightning_logs/version_1/checkpoints/epoch=499-step=519000.ckpt \
  ~/piper_MODELS_EXPORTED/my_ljspeech_piper_voice.onnx

この時にモデルの設定ファイルもコピーしておきます

cp /data/piper_ljspeech_training/config.json \
  ~/piper_MODELS_EXPORTED/my_ljspeech_piper_voice.onnx.json

学習したモデルから推論

onnxに変換したモデルから実際に音声を生成してみます 英語のみの音声データのため、英語のテキストを使って音声合成を行います。

echo "Hello, this is a test of the trained Piper model." | \
  piper \
    -m ./${ONNX_MODEL_NAME}.onnx \
    --output_file output.wav