piper-plusの日本語TTSをOpenJTalkのアクセントラベルで改善してみた

初めに

日本語のTTSでは、音素の並びだけでなくアクセントやイントネーションが自然さに大きく影響します。例えば「雨」と「飴」は同じ音素列 /a m e/ ですが、アクセントパターンが異なります。

piper-plusでは、espeak-ngによる汎用的なフォネマイズに加えて、OpenJTalkのフルコンテキストラベルから抽出したアクセント情報(A1/A2/A3)を学習パイプラインに注入できます。今回はこの仕組みを使って日本語TTSの品質改善を試みます。

この機能は PR #196 で実装され、v1.6.0(2026-03-04リリース)から利用可能です。v1.5.5以前には含まれていないため、利用する場合はv1.6.0以降にアップデートしてください。

github.com

開発環境

  • OS: Windows 11
  • GPU: NVIDIA RTX 4090
  • Python: 3.12
  • パッケージマネージャー: uv
  • CUDA: 12.x
  • piper-plus: 1.10.0

OpenJTalkのフルコンテキストラベルとは

OpenJTalkは日本語テキストを解析し、各音素に対してHTS形式のフルコンテキストラベルを生成します。pyopenjtalk-plus を使うと以下のようにラベルを取得できます。

import pyopenjtalk

labels = pyopenjtalk.extract_fullcontext("こんにちは")
for label in labels:
    print(label)

出力例:

xx^xx-sil+k=o/A:xx+xx+xx/B:xx-xx_xx/C:xx_xx+xx/D:xx+xx_xx/E:xx_xx!xx_xx-xx/F:xx_xx#xx_xx@xx_xx|xx_xx/G:5_5%0_0_xx/H:xx_xx/I:xx-xx@xx+xx&xx-xx|xx+xx/J:1_5/K:1+1-5
xx^sil-k+o=N/A:-4+1+5/B:xx-xx_xx/C:09_xx+xx/D:xx+xx_xx/E:xx_xx!xx_xx-xx/F:5_5#0_0@1_1|1_5/G:xx_xx%xx_xx_xx/H:xx_xx/I:1-5@1+1&1-1|1+5/J:xx_xx/K:1+1-5
sil^k-o+N=n/A:-4+1+5/B:xx-xx_xx/C:09_xx+xx/D:xx+xx_xx/E:xx_xx!xx_xx-xx/F:5_5#0_0@1_1|1_5/G:xx_xx%xx_xx_xx/H:xx_xx/I:1-5@1+1&1-1|1+5/J:xx_xx/K:1+1-5
k^o-N+n=i/A:-3+2+4/B:xx-xx_xx/C:09_xx+xx/D:xx+xx_xx/E:xx_xx!xx_xx-xx/F:5_5#0_0@1_1|1_5/G:xx_xx%xx_xx_xx/H:xx_xx/I:1-5@1+1&1-1|1+5/J:xx_xx/K:1+1-5
o^N-n+i=ch/A:-2+3+3/B:xx-xx_xx/C:09_xx+xx/D:xx+xx_xx/E:xx_xx!xx_xx-xx/F:5_5#0_0@1_1|1_5/G:xx_xx%xx_xx_xx/H:xx_xx/I:1-5@1+1&1-1|1+5/J:xx_xx/K:1+1-5
N^n-i+ch=i/A:-2+3+3/B:xx-xx_xx/C:09_xx+xx/D:xx+xx_xx/E:xx_xx!xx_xx-xx/F:5_5#0_0@1_1|1_5/G:xx_xx%xx_xx_xx/H:xx_xx/I:1-5@1+1&1-1|1+5/J:xx_xx/K:1+1-5
n^i-ch+i=w/A:-1+4+2/B:xx-xx_xx/C:09_xx+xx/D:xx+xx_xx/E:xx_xx!xx_xx-xx/F:5_5#0_0@1_1|1_5/G:xx_xx%xx_xx_xx/H:xx_xx/I:1-5@1+1&1-1|1+5/J:xx_xx/K:1+1-5
i^ch-i+w=a/A:0+5+1/B:xx-xx_xx/C:09_xx+xx/D:xx+xx_xx/E:xx_xx!xx_xx-xx/F:5_5#0_0@1_1|1_5/G:xx_xx%xx_xx_xx/H:xx_xx/I:1-5@1+1&1-1|1+5/J:xx_xx/K:1+1-5
ch^i-w+a=sil/A:0+5+1/B:xx-xx_xx/C:09_xx+xx/D:xx+xx_xx/E:xx_xx!xx_xx-xx/F:5_5#0_0@1_1|1_5/G:xx_xx%xx_xx_xx/H:xx_xx/I:1-5@1+1&1-1|1+5/J:xx_xx/K:1+1-5
i^w-a+sil=xx/A:0+5+1/B:xx-xx_xx/C:09_xx+xx/D:xx+xx_xx/E:xx_xx!xx_xx-xx/F:5_5#0_0@1_1|1_5/G:xx_xx%xx_xx_xx/H:xx_xx/I:1-5@1+1&1-1|1+5/J:xx_xx/K:1+1-5
w^a-sil+xx=xx/A:xx+xx+xx/B:xx-xx_xx/C:xx_xx+xx/D:xx+xx_xx/E:5_5!0_0-xx/F:xx_xx#xx_xx@xx_xx|xx_xx/G:xx_xx%xx_xx_xx/H:1_5/I:xx-xx@xx+xx&xx-xx|xx+xx/J:xx_xx/K:1+1-5

piper-plusが利用するのは /A: セクションの3つの数値です。

フィールド 意味 説明
A1 モーラ位置 アクセント句内でのモーラの位置
A2 アクセント核距離 アクセント核からのモーラ数
A3 アクセント句長 アクセント句内のモーラ総数

これらの数値は以下の正規表現で抽出されます。

import re

_RE_A1 = re.compile(r"/A:([\d-]+)\+")   # A1: モーラ位置
_RE_A2 = re.compile(r"\+([0-9]+)\+")     # A2: アクセント核距離
_RE_A3 = re.compile(r"\+([0-9]+)/")      # A3: アクセント句長

韻律記号への変換

piper-plusではA1/A2/A3の値から韻律記号を生成し、フォネム列に挿入します。変換ルールは以下の通りです。

条件 挿入記号 意味
a1 == 0 かつ a2_next == a2 + 1 ] アクセント核(下降)
a2 == a3 かつ a2_next == 1 # アクセント句境界
a2 == 1 かつ a2_next == 2 [ 上昇
文頭の sil ^ 発話開始
文末の sil $ 発話終了
文末が疑問形 ? 疑問マーカー
pau(ポーズ) _ 短い休止

例えば「こんにちは」は以下のような韻律付きフォネム列に変換されます。

^ k o [N n i ch i w a] $

[ で音程が上昇し、] でアクセント核の下降が示されます。

「N」の文脈依存変異

日本語の撥音「ん」は、後続する音素によって発音が変化します。piper-plusではこの変異を4種類に区別しています。

変異 条件
N_m 両唇音(m, b, p)の前 さんぽ → s a N_m p o
N_n 歯茎音(n, t, d, ts, ch)の前 あんない → a N_n n a i
N_ng 軟口蓋音(k, g)の前 りんご → r i N_ng g o
N_uvular 句末・母音の前 パン → p a N_uvular

この区別により、より自然な発音が生成されます。

環境構築

学習パイプラインを使うため、trainのextraをインストールします。

uv add "piper-plus[train]"

pyopenjtalk-plusとNAIST日本語辞書は依存関係として自動的にインストールされます。

前処理でのラベル適用

espeak-ng(ラベルなし)での前処理

比較のため、まずespeak-ngで前処理を実行します。

uv run python -m piper_train.preprocess \
  --language en \
  --input-dir ./dataset/ \
  --output-dir ./training_espeak/ \
  --dataset-format ljspeech \
  --single-speaker \
  --sample-rate 22050

この場合、韻律情報は含まれません。

OpenJTalk(ラベルあり)での前処理

--language ja を指定すると、自動的にOpenJTalkフォネマイザーに切り替わり、A1/A2/A3の韻律情報が生成されます。

uv run python -m piper_train.preprocess \
  --language ja \
  --input-dir ./dataset/ \
  --output-dir ./training_openjtalk/ \
  --dataset-format ljspeech \
  --single-speaker \
  --sample-rate 22050

出力される dataset.jsonl には prosody_features フィールドが追加されます。

{
  "phoneme_ids": [1, 45, 67, 23, ...],
  "audio_norm_path": "audio/0001.norm.pt",
  "audio_spec_path": "audio/0001.spec.pt",
  "text": "こんにちは",
  "phonemes": "^ k o [N_uvular n i ch i w a] $",
  "prosody_features": [[0, 2, 4], [0, 2, 4], [1, 1, 4], ...]
}

prosody_features は各フォネム位置に対応する [a1, a2, a3] のリストで、学習時にテンソル形状 [1, seq_len, 3] として Duration Predictor に入力されます。

学習への韻律情報注入

学習時に --prosody-dim 16 を指定すると、A1/A2/A3の韻律情報がDuration Predictorに注入されます。

uv run python -m piper_train \
  --dataset-dir ./training_openjtalk/ \
  --accelerator gpu --devices 1 \
  --precision 16-mixed \
  --max_epochs 200 \
  --batch-size 16 \
  --quality medium \
  --prosody-dim 16 \
  --ema-decay 0.9995

--prosody-dim 0 を指定するか、prosody_featuresのないデータセットを使うと韻律注入が無効になります。

所感

日本語TTSを学習する場合は --language ja で前処理を行い、--prosody-dim 16 を有効にすることを推奨します。次回はpiper-plusで自分の音声データを使った追加学習(ファインチューニング)を試みます。