- 初めに
- 開発環境
- 環境構築
- CLIの基本的な使い方
- 8言語G2P
- Rust APIでの合成
- ストリーミング合成
- フォネムタイミング
- GPU推論(feature gates)
- WASMターゲット
- PyO3 Pythonバインディング
- 実行結果
- 所感
初めに
今回は、piper-plus v1.11.0のRust SDKについて紹介します。Rust SDKはメモリ安全で高速な推論に加え、feature gatesによる柔軟な機能選択が特徴です。
crates.ioで公開されているクレートは以下の3つです。すべてワークスペースバージョン 0.2.0 を共有しています。
| クレート | crates.io | 説明 |
|---|---|---|
piper-plus |
v0.2.0 | コアライブラリ(推論エンジン、G2P統合、ストリーミング) |
piper-plus-cli |
v0.2.0 | CLIツール |
piper-plus-g2p |
v0.2.0 | 独立G2Pパッケージ(eSpeak-ng不要) |
cargo install piper-plus-cli で即座に使い始められます。
開発環境
- OS: Windows 11
- Rust: 1.88+(edition 2024)
- GPU: NVIDIA RTX 4090(CUDA使用時)
- ONNX Runtime:
ortクレートが自動ダウンロード
環境構築
CLIのインストール
cargo install piper-plus-cli
ort クレート(v2.0.0-rc.12)がビルド時にONNX Runtimeの共有ライブラリを自動ダウンロードするため、手動でのセットアップは不要です。download-binaries featureがデフォルトで有効になっています。
モデルのダウンロード
利用可能なモデルの一覧を確認し、ダウンロードします。
# 日本語モデルの一覧 piper-plus-cli --list-models ja
Available models: tsukuyomi-6lang-v2 (ja-en-zh-es-fr-pt) - Tsukuyomi-chan 6-language model (JA/EN/ZH/ES/FR/PT) css10-6lang (ja-en-zh-es-fr-pt) - CSS10 Japanese 6-language model fine-tuned from multilingual base (FP16)
# つくよみちゃんモデルをダウンロード piper-plus-cli --download-model tsukuyomi
Downloading model: tsukuyomi to %APPDATA%\piper-plus\models Downloading... 100.0% Model saved to: %APPDATA%\piper-plus\models\tsukuyomi-chan-6lang-fp16.onnx Config saved to: %APPDATA%\piper-plus\models\config.json
CLIの基本的な使い方
テキストから音声合成
# 日本語の音声合成(-q でログを抑制) piper-plus-cli -m tsukuyomi --text "こんにちは、今日は良い天気ですね。" -f output.wav -q
初回実行時にモデルが未ダウンロードの場合は自動でダウンロードされます。-q(--quiet)を付けないとONNX Runtimeの詳細ログが表示されるため、通常は付けることを推奨します。
パラメータの調整
# ゆっくり話す piper-plus-cli -m tsukuyomi --text "ゆっくり話してみます。" -f output.wav --length-scale 1.3 --noise-scale 0.5 --noise-w 0.6 # 話者IDの指定(マルチスピーカーモデルの場合) piper-plus-cli -m tsukuyomi --text "Hello" -f output.wav --speaker 3 --language en
デバイス指定
# 自動検出(CUDA → CoreML → DirectML → CPU) piper-plus-cli -m tsukuyomi --text "テスト" -f output.wav --device auto # CUDA を明示指定 piper-plus-cli -m tsukuyomi --text "テスト" -f output.wav --device cuda # CPU を明示指定 piper-plus-cli -m tsukuyomi --text "テスト" -f output.wav --device cpu
raw PCM出力
WAVヘッダなしのraw PCMを標準出力に流し、外部プレイヤーにパイプできます。
piper-plus-cli -m tsukuyomi --text "パイプで再生します" --output-raw | aplay -r 22050 -f S16_LE
バッチ処理
テキストファイルから1行ずつ読み込んで合成します。
piper-plus-cli -m tsukuyomi --batch input.txt -d output/ --language ja
CLIの全オプションは以下の通りです。
| フラグ | デフォルト | 説明 |
|---|---|---|
-m, --model |
ONNXモデルのパスまたはモデル名 | |
-c, --config |
自動検出 | config.jsonのパス |
--text |
合成するテキスト | |
-l, --language |
自動検出 | 言語コード(ja, en, zh, ko, es, fr, pt, sv) |
-s, --speaker |
0 | 話者ID |
-f, --output-file |
WAV出力パス(- でstdout) |
|
-d, --output-dir |
出力ディレクトリ | |
--noise-scale |
0.667 | 生成ノイズスケール |
--length-scale |
1.0 | 発話速度 |
--noise-w |
0.8 | 音素長ノイズ |
--device |
auto | 推論デバイス |
--stream |
false | ストリーミング合成 |
--timing |
フォネムタイミング出力(json/tsv/srt) | |
--custom-dict |
カスタム辞書パス(複数指定可) | |
--output-raw |
false | raw PCM int16をstdoutに出力 |
--batch |
バッチファイル(1行1発話) | |
--sentence-silence |
0.2 | 文間の無音秒数 |
--phoneme-silence |
特定音素の後に追加する無音(例: "_ 0.5") |
|
--list-models |
モデル一覧(言語フィルタ可) | |
--list-devices |
利用可能なデバイス一覧 | |
--download-model |
モデルをダウンロード | |
--model-dir |
モデルディレクトリ(ダウンロード先) | |
--test-mode |
false | 推論スキップ、phoneme IDsのみ出力 |
--no-warmup |
false | 起動時のORT warmupを無効化 |
--debug |
false | デバッグログ出力 |
-q, --quiet |
false | ログ出力を無効化 |
8言語G2P
piper-plus-g2p クレート(v0.2.0、crates.io公開済み)はeSpeak-ng不要のMITライセンスG2Pを提供します。
対応言語とfeature flags
[dependencies] piper-plus-g2p = { version = "0.2.0", features = ["all-languages"] }
| 言語 | feature flag | デフォルト | 実装方式 |
|---|---|---|---|
| 日本語(JA) | japanese / naist-jdic |
No | jpreprocess(Rust純粋OpenJTalk互換) |
| 英語(EN) | english |
Yes | CMU辞書134K語 + 形態素フォールバック |
| 中国語(ZH) | chinese |
Yes | ピンイン変換(単漢字42K + 多音字110K) |
| 韓国語(KO) | korean |
Yes | ハングル算術分解 + 音韻規則 |
| スペイン語(ES) | spanish |
Yes | 規則ベース(依存なし) |
| フランス語(FR) | french |
Yes | 規則ベース(依存なし) |
| ポルトガル語(PT) | portuguese |
Yes | 規則ベース(依存なし) |
| スウェーデン語(SV) | swedish |
Yes | 規則ベース(依存なし) |
日本語の jpreprocess はRustで書かれたOpenJTalk互換ライブラリで、C言語のOpenJTalkをFFIで呼び出すのではなく、Rust純粋実装として動作します。これにより、WASMを含む全ターゲットで一貫したビルドが可能です。naist-jdic featureを有効にすると、NAIST-JDIC辞書がバイナリに埋め込まれます。
必要な言語だけを選択してバイナリサイズを抑えることもできます。
# 日本語と英語のみ [dependencies] piper-plus-g2p = { version = "0.2.0", default-features = false, features = ["naist-jdic", "english"] }
G2P APIの使い方
use piper_plus_g2p::Phonemizer; use piper_plus_g2p::japanese::JapanesePhonemizer; use piper_plus_g2p::english::EnglishPhonemizer; fn main() -> Result<(), Box<dyn std::error::Error>> { // 日本語(bundled NAIST-JDIC辞書を使用、feature = "naist-jdic" 必須) let ja = JapanesePhonemizer::new_bundled()?; let (tokens, prosody) = ja.phonemize_with_prosody("こんにちは")?; println!("JA tokens: {:?}", tokens); // ["k", "o", "ɴ", "n", "i", "ch", "i", "w", "a"] // 英語(CMU辞書を自動検索: CMUDICT_PATH env → ./cmudict_data.json → /usr/share/piper/) let en = EnglishPhonemizer::new()?; let (tokens, _) = en.phonemize_with_prosody("Hello, world!")?; println!("EN tokens: {:?}", tokens); // ["h", "ʌ", "l", "oʊ", ",", " ", "w", "ɝ", "l", "d", "!"] Ok(()) }
PhonemizerRegistry を使って複数言語のPhonemizerをまとめて管理できます。
use piper_plus_g2p::{Phonemizer, PhonemizerRegistry}; use piper_plus_g2p::english::EnglishPhonemizer; let mut registry = PhonemizerRegistry::new(); let en = EnglishPhonemizer::new()?; registry.register("en", Box::new(en)); let phonemizer = registry.get("en").unwrap(); let (tokens, prosody) = phonemizer.phonemize_with_prosody("Hello, world!")?;
多言語テキストの自動言語検出にも対応しています。MultilingualPhonemizer はUnicode文字範囲に基づいてテキストを言語セグメントに分割し、各セグメントを適切なPhonemizerに委譲します。
use piper_plus_g2p::multilingual::MultilingualPhonemizer; // 複数言語の Phonemizer を登録 let mut phonemizers = std::collections::HashMap::new(); phonemizers.insert("ja".to_string(), Box::new(ja) as Box<dyn piper_plus_g2p::Phonemizer>); phonemizers.insert("en".to_string(), Box::new(en) as Box<dyn piper_plus_g2p::Phonemizer>); let multi = MultilingualPhonemizer::new( vec!["ja".to_string(), "en".to_string()], "en".to_string(), // ラテン文字のデフォルト言語 phonemizers, ); // カナ → 日本語、ラテン → 英語として自動判定 let (tokens, prosody) = multi.phonemize_with_prosody("こんにちは、Hello!")?;
Rust APIでの合成
PiperVoice がテキストから音声合成までの高レベルAPIを提供します。
use std::path::Path; use piper_plus::PiperVoice; fn main() -> Result<(), Box<dyn std::error::Error>> { // モデルの読み込み // phonemizer と engine の初期化は自動的に並列化される let mut voice = PiperVoice::load( Path::new("tsukuyomi-chan-6lang-fp16.onnx"), None, // config は自動検出 "auto", // デバイス(auto で GPU 自動検出) )?; // テキストから直接合成(phonemize + inference) let result = voice.synthesize_text( "こんにちは、今日は良い天気ですね。", Some(0), // speaker_id Some("ja"), // language_override(None で自動検出) 0.667, // noise_scale 1.0, // length_scale 0.8, // noise_w )?; // 結果の確認 println!("サンプルレート: {}", result.sample_rate); println!("音声長: {:.2}秒", result.audio_seconds); println!("推論時間: {:.3}秒", result.infer_seconds); println!("RTF: {:.3}", result.real_time_factor()); // WAV ファイルに書き出し let spec = hound::WavSpec { channels: 1, sample_rate: result.sample_rate, bits_per_sample: 16, sample_format: hound::SampleFormat::Int, }; let mut writer = hound::WavWriter::create("output.wav", spec)?; for sample in &result.audio { writer.write_sample(*sample)?; } writer.finalize()?; Ok(()) }
PiperVoice::load() は内部でphonemizerとONNXエンジンの初期化を std::thread::spawn で並列化しています。これにより初期化時間が max(phonemizer_time, engine_time) に短縮されます(WASM環境では逐次実行にフォールバック)。
便利メソッドとして text_to_wav_file() も提供されています。デフォルトパラメータ(noise_scale=0.667, length_scale=1.0, noise_w=0.8)で直接WAVファイルに出力します。
voice.text_to_wav_file("こんにちは", Path::new("output.wav"), Some(0))?;
SynthesisRequest と SynthesisResult の構造は以下の通りです。
/// 合成パラメータ(低レベルAPI用) pub struct SynthesisRequest { pub phoneme_ids: Vec<i64>, pub prosody_features: Option<Vec<[i32; 3]>>, // プロソディ情報 pub speaker_id: Option<i64>, pub language_id: Option<i64>, pub noise_scale: f32, pub length_scale: f32, pub noise_w: f32, } /// 合成結果 pub struct SynthesisResult { pub audio: Vec<i16>, // PCMサンプル(モノラル、16-bit) pub sample_rate: u32, // サンプルレート(例: 22050) pub infer_seconds: f64, // 推論時間(秒) pub audio_seconds: f64, // 音声の長さ(秒) pub durations: Option<Vec<f32>>, // 音素ごとのフレーム数(タイミング用) } impl SynthesisResult { // RTF < 1.0 ならリアルタイムより高速 pub fn real_time_factor(&self) -> f64; }
ストリーミング合成
AudioSink トレイトを実装することで、センテンス単位の逐次合成が可能です。
use piper_plus::streaming::{AudioSink, BufferSink, WavFileSink}; use piper_plus::error::PiperError; // AudioSink トレイトの定義 pub trait AudioSink { fn write_chunk(&mut self, samples: &[i16], sample_rate: u32) -> Result<(), PiperError>; fn finalize(&mut self) -> Result<(), PiperError>; }
組み込みの実装として BufferSink(メモリ内バッファ)と WavFileSink(WAVファイル直接書き込み)が提供されています。
use std::path::Path; use piper_plus::streaming::{BufferSink, WavFileSink}; // メモリバッファに蓄積 let mut buffer_sink = BufferSink::new(); // WAVファイルに逐次書き込み let mut wav_sink = WavFileSink::new(Path::new("output.wav"))?;
WavFileSink は最初の write_chunk 呼び出し時にWAVヘッダを書き込み、finalize() でファイルサイズを更新します。Drop トレイトも実装されているため、finalize() を呼び忘れても安全です。
CLIからのストリーミング使用例です。
# センテンス単位で逐次合成(--stream には -d が必須) piper-plus-cli -m tsukuyomi --text "最初の文です。次の文です。最後の文です。" --stream -d chunks/ -q # raw PCMをパイプして逐次再生 piper-plus-cli -m tsukuyomi --text "ストリーミングで再生します。リアルタイムに聴けます。" --stream --output-raw | aplay -r 22050 -f S16_LE
フォネムタイミング
VITSモデルのDuration Predictorが出力するフレーム数を時間情報に変換し、音素ごとの開始・終了時刻を取得できます。
# JSON形式で出力 piper-plus-cli -m tsukuyomi --text "こんにちは" --timing json
{ "phonemes": [ {"phoneme": "ph_0", "start_ms": 0.000, "end_ms": 6.122, "duration_ms": 6.122}, {"phoneme": "ph_1", "start_ms": 6.122, "end_ms": 20.248, "duration_ms": 14.126}, {"phoneme": "ph_2", "start_ms": 20.248, "end_ms": 31.106, "duration_ms": 10.858}, ... ], "total_duration_ms": 553.357, "sample_rate": 22050 }
# TSV形式(スクリプト処理向き) piper-plus-cli -m tsukuyomi --text "こんにちは" --timing tsv
start_ms end_ms duration_ms phoneme 0.000 6.122 6.122 ph_0 6.122 20.248 14.126 ph_1 20.248 31.106 10.858 ph_2 31.106 44.400 13.293 ph_3 ...
# SRT形式(字幕ファイル) piper-plus-cli -m tsukuyomi --text "こんにちは" --timing srt
1 00:00:00,000 --> 00:00:00,006 ph_0 2 00:00:00,006 --> 00:00:00,020 ph_1 3 00:00:00,020 --> 00:00:00,031 ph_2
フォネムタイミングはリップシンク同期、字幕生成、カラオケ表示などに活用できます。PhonemeTimingInfo と TimingResult の構造は以下の通りです。
pub struct PhonemeTimingInfo { pub phoneme: String, pub start_ms: f64, pub end_ms: f64, pub duration_ms: f64, } pub struct TimingResult { pub phonemes: Vec<PhonemeTimingInfo>, pub total_duration_ms: f64, pub sample_rate: u32, } impl TimingResult { pub fn to_json(&self) -> Result<String, PiperError>; pub fn to_json_compact(&self) -> Result<String, PiperError>; pub fn to_tsv(&self) -> String; pub fn to_srt(&self) -> String; }
GPU推論(feature gates)
GPU推論はfeature flagsで制御されます。piper-plus クレートのCargo.tomlに以下のfeatureが定義されています。
[features] default = ["naist-jdic", "dict-download"] onnx = ["dep:ort", "dep:ndarray", "dep:hound"] inference = ["onnx"] # backward compat alias japanese = ["dep:jpreprocess", "piper-plus-g2p/japanese"] naist-jdic = ["japanese", "jpreprocess/naist-jdic", "piper-plus-g2p/naist-jdic"] playback = ["dep:rodio"] download = ["dep:reqwest"] dict-download = ["download", "dep:sha2", "dep:flate2", "dep:tar"] resample = ["dep:rubato"] cuda = [] coreml = [] directml = [] tensorrt = []
CLIでは --device フラグで指定します。
# CUDA piper-plus-cli -m tsukuyomi --text "CUDAで合成" --device cuda -f output.wav # CoreML(macOS) piper-plus-cli -m tsukuyomi --text "CoreMLで合成" --device coreml -f output.wav # DirectML(Windows) piper-plus-cli -m tsukuyomi --text "DirectMLで合成" --device directml -f output.wav # 自動検出(利用可能なGPUを順に試行、失敗時はCPUにフォールバック) piper-plus-cli -m tsukuyomi --text "自動検出" --device auto -f output.wav
--device auto はデフォルト値で、CUDA / CoreML / DirectML の順に検出を試み、いずれも利用できない場合はCPUにフォールバックします。
ライブラリから使う場合は PiperVoice::load() の第3引数にデバイス文字列を渡します。
// CUDA let voice = PiperVoice::load(Path::new("model.onnx"), None, "cuda")?; // 自動検出 let voice = PiperVoice::load(Path::new("model.onnx"), None, "auto")?;
WASMターゲット
piper-plus-wasm クレートはブラウザ向けのG2P + 合成を提供します。ファイルシステムに依存しない設計で、モデルやconfig はバイトスライスとして受け取ります。
feature flags
[features] default = [] multilingual = ["ja", "zh", "ko", "es", "fr", "pt", "sv"] multilingual-external = ["ja-external", "zh-external", "ko", "es", "fr", "pt", "sv"] ja = ["piper-plus-g2p/naist-jdic"] # JA辞書をバイナリに埋め込み ja-external = ["piper-plus-g2p/japanese"] # JA辞書を実行時に外部から渡す zh = ["piper-plus-g2p/chinese"] # ZH G2P有効化(辞書はランタイムロード) zh-external = ["piper-plus-g2p/chinese"] # zh と同等(命名の対称性のため)
| バリアント | WASM サイズ | 用途 |
|---|---|---|
multilingual |
~58MB | 全機能(辞書埋め込み) |
ja (ja-only) |
~57.5MB | 日本語のみ(辞書埋め込み) |
ja-external (ja-lite) |
~2MB | 日本語(辞書を実行時にダウンロード) |
ja-external バリアントでは辞書をバイナリに埋め込まないため、WASMサイズを大幅に削減できます。辞書はIndexedDBにキャッシュされ、2回目以降のアクセスではダウンロードが発生しません。
ビルドには wasm-pack を使います。
# ja-lite バリアント(2MB) cd src/rust/piper-wasm wasm-pack build --release --features ja-external # 全言語バリアント wasm-pack build --release --features multilingual
WASM版のAPIはJavaScript/TypeScript側から wasm-bindgen を介して呼び出します。
PyO3 Pythonバインディング
piper-plus-python クレートはPyO3を使ったPythonバインディングです。Rust実装の高速な推論エンジンをPythonから直接呼び出せます。
[dependencies] piper_core = { package = "piper-plus", path = "../piper-core", version = "0.2.0", features = ["naist-jdic", "onnx"] } pyo3 = { version = "0.24", features = ["extension-module"] } numpy = "0.24"
numpy クレートとの連携により、合成結果のPCMデータをNumPy配列として効率的にPython側に渡せます。コピーを最小限に抑えた設計になっています。
# PyO3バインディング経由でRust推論エンジンを使用 import piper_plus voice = piper_plus.PiperVoice("tsukuyomi-chan-6lang-fp16.onnx", device="cpu") result = voice.synthesize("こんにちは", language="ja") # NumPy配列として取得 audio = result.audio_int16() # numpy.ndarray (int16) # audio = result.audio_float32() # numpy.ndarray (float32, [-1.0, 1.0]) print(f"サンプル数: {len(audio)}, サンプルレート: {result.sample_rate}")
実行結果
CPU環境でのReal-Time Factor(RTF)を計測しました。RTFが1.0未満であれば実時間より高速に合成できていることを意味します。
piper-plus-cli -m tsukuyomi --text "こんにちは、今日は良い天気ですね。音声合成のテストをしています。" -f output.wav --device cpu
Synthesized: 3.866s audio, 0.168s infer, RTF=0.043
| デバイス | RTF | 備考 |
|---|---|---|
| CPU (Ryzen 9 5900X) | 0.043 | 実時間の約23倍速 |
RTF 0.043は、3.87秒の音声を0.17秒で合成できたことを意味します。CPUのみでもリアルタイムの23倍以上の速度が出ています。CUDA featureを有効にしたビルドではさらに高速化が期待できます。
所感
Rust SDKの設計で特にこだわったのは jpreprocess の採用です。日本語G2PにC言語のOpenJTalkを使わずRust純粋実装で処理することで、WASMを含む全ターゲットで一貫したビルドを実現しています。CGO不要でクロスコンパイルも容易です。
feature gatesによる柔軟な機能選択も意識して設計しました。日本語のみのアプリケーションでは features = ["naist-jdic", "onnx"] だけを有効にしてバイナリサイズを抑えられますし、WASM向けには ja-external で辞書を外部化して2MBまで軽量化できます。全言語が必要なサーバーでは all-languages を有効にすれば済みます。用途に応じて依存を最小化できるため、組み込みからサーバーまで幅広いデプロイ先に対応できます。
v1.11.0で全Rustクレート(piper-plus、piper-plus-cli、piper-plus-g2p)をcrates.ioに公開し、cargo install や cargo add でのセットアップが格段に簡単になりました。