初めに
日本語の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以降にアップデートしてください。
開発環境
- 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で自分の音声データを使った追加学習(ファインチューニング)を試みます。