piper-plus v1.11.0のGo SDKでTTS APIサーバーを構築する

初めに

今回は、piper-plusのGo SDKを使ったHTTP APIのTTSサーバー構築方法を解説します。Go SDKはシングルバイナリでデプロイでき、VoicePoolによる並行合成、6言語対応のG2P、ストリーミング合成、GPU推論に対応しています。

github.com

開発環境

  • OS: Windows 11 / Linux(Docker)
  • Go: 1.26+
  • ONNX Runtime: 1.21.0
  • GPU: NVIDIA RTX 4090(CUDA使用時)
  • piper-plus: v1.11.0

アーキテクチャ概要

Go SDKは4層の構造になっています。

Init/Shutdown  ← ONNX Runtime のライフサイクル管理
    ↓
  Voice        ← モデルの読み込みと合成
    ↓
 VoicePool     ← 並行セッション管理(セマフォ + 遅延生成)
    ↓
  Server       ← HTTP API エンドポイント

G2Pは6言語(JA/EN/ZH/ES/FR/PT)に対応しています。日本語はCGO経由でOpenJTalkを呼び出し、ピッチアクセント情報を含む高品質な音素化を行います。韓国語(KO)とスウェーデン語(SV)はG2Pの実装はありますが、対応する学習済みモデルがまだ公開されていないため、現時点では利用できません。

環境構築

ONNX Runtimeの準備

Go SDKはONNX Runtimeの共有ライブラリを必要とします。ONNX Runtime のリリースページからダウンロードしてください。

OS ファイル名
Linux libonnxruntime.so
macOS libonnxruntime.dylib
Windows onnxruntime.dll

環境変数でパスを設定します。

# Linux / macOS
export ONNX_RUNTIME_SHARED_LIBRARY_PATH=/usr/lib/libonnxruntime.so

# Windows
set ONNX_RUNTIME_SHARED_LIBRARY_PATH=C:\onnxruntime\onnxruntime.dll

CLIのビルド

Go SDKにはCLIツールを同梱しています。go.modreplace ディレクティブを含めているため、go install は使えません。リポジトリをクローンしてビルドします。

重要: CGO_ENABLED=1 が必須です。 ONNX Runtimeのバインディングに加え、日本語のG2P(OpenJTalk)がCGO経由でネイティブライブラリを呼び出すため、CGOが無効な環境ではビルドが失敗します。WindowsではMSYS2/MinGW、Linuxではgcc/musl-devなどのCコンパイラが必要です。

git clone https://github.com/ayutaz/piper-plus.git
cd piper-plus/src/go
CGO_ENABLED=1 go build -o piper-plus ./cmd/piper-plus

ビルドしたバイナリをPATHの通った場所に配置するか、make install でGOPATH/binにインストールすることもできます。

make install

モデルのダウンロード

CLIからモデルの一覧表示とダウンロードができます。

# キャッシュ済みモデルの一覧
piper-plus --list-models

# URLを指定してモデルをダウンロード
piper-plus --download-model https://huggingface.co/ayutaz/tsukuyomi-chan-6lang-v2/resolve/main/tsukuyomi-chan-6lang-fp16.onnx

Go APIの基本的な使い方

まず、最小限の合成プログラムを書いてみます。

package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/ayutaz/piper-plus/src/go/piperplus"
)

func main() {
    // ONNX Runtime を初期化
    // 引数が空文字の場合、ONNX_RUNTIME_SHARED_LIBRARY_PATH 環境変数を参照
    if err := piperplus.Init(""); err != nil {
        log.Fatal(err)
    }
    defer piperplus.Shutdown()

    // モデルを読み込み(config.json は model.onnx.json から自動検出)
    ctx := context.Background()
    voice, err := piperplus.LoadVoice(ctx, "tsukuyomi-chan-6lang-fp16.onnx")
    if err != nil {
        log.Fatal(err)
    }
    defer voice.Close()

    // 日本語で音声合成
    result, err := voice.Synthesize(ctx, "こんにちは、今日は良い天気ですね。",
        piperplus.WithLanguage("ja"),
    )
    if err != nil {
        log.Fatal(err)
    }

    // WAV ファイルに書き出し
    f, err := os.Create("output.wav")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()

    if _, err := result.WriteTo(f); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("RTF: %.3f (%.2f秒の音声を%.2f秒で合成)\n",
        result.RTF(), result.Duration.Seconds(), result.InferTime.Seconds())
}

piperplus.Init("") はスレッドセーフで、最初の呼び出しのみ有効です。LoadVoice() はconfig.jsonをモデルファイルのサイドカー(model.onnx.json または同ディレクトリの config.json)から自動検出します。

SynthesisResult には以下のフィールドとメソッドがあります。

フィールド / メソッド 説明
Audio []int16 PCMサンプル(モノラル、16-bit、ピーク正規化)
SampleRate int サンプルレート(例: 22050)
Duration time.Duration 音声の長さ
InferTime time.Duration 推論にかかった時間
RTF() float64 Real-Time Factor(推論時間 / 音声時間、1.0未満なら実時間より高速)
WriteTo(w) (int64, error) WAVを書き出し(io.WriterTo 実装)
RawPCMReader() io.Reader 生PCM int16バイト列のReader

多言語の例です。

// 英語
result, _ := voice.Synthesize(ctx, "Hello, how are you today?",
    piperplus.WithLanguage("en"))

// 中国語
result, _ := voice.Synthesize(ctx, "你好,今天天气很好。",
    piperplus.WithLanguage("zh"))

// スペイン語
result, _ := voice.Synthesize(ctx, "Hola, ¿cómo estás?",
    piperplus.WithLanguage("es"))

パラメータも調整できます。

result, err := voice.Synthesize(ctx, "ゆっくり話してみます。",
    piperplus.WithLanguage("ja"),
    piperplus.WithSpeakerID(0),
    piperplus.WithNoiseScale(0.5),
    piperplus.WithLengthScale(1.3),   // ゆっくり
    piperplus.WithNoiseW(0.6),
)

HTTP APIサーバー

Go SDKにHTTP APIサーバーを組み込んでいます。CLIの serve サブコマンド、またはGoプログラムから NewServer() を使って起動できます。

CLIからの起動

piper-plus serve -m tsukuyomi-chan-6lang-fp16.onnx --addr :8080

--model--device--custom-dict などのフラグは serve サブコマンドでも共通で使えます。

Goプログラムからの起動

package main

import (
    "context"
    "log"
    "log/slog"

    "github.com/ayutaz/piper-plus/src/go/piperplus"
)

func main() {
    if err := piperplus.Init(""); err != nil {
        log.Fatal(err)
    }
    defer piperplus.Shutdown()

    ctx := context.Background()
    voice, err := piperplus.LoadVoice(ctx, "tsukuyomi-chan-6lang-fp16.onnx")
    if err != nil {
        log.Fatal(err)
    }
    defer voice.Close()

    logger := slog.Default()
    server := piperplus.NewServer(voice, logger)

    log.Println("TTS server starting on :8080")
    if err := server.ListenAndServe(":8080"); err != nil {
        log.Fatal(err)
    }
}

エンドポイント

エンドポイント メソッド 説明
/synthesize GET / POST テキストを受け取りWAVを返す
/health GET ヘルスチェック({"status":"ok"}
/info GET モデル情報(話者数、言語、サンプルレート等)

curlでの動作確認

GETリクエストでの合成です。GETのクエリパラメータは langspeaker を使います(POSTのJSONボディでは languagespeaker_id)。

# 日本語の音声合成
curl "http://localhost:8080/synthesize?text=こんにちは&lang=ja" -o output.wav

# 英語の音声合成
curl "http://localhost:8080/synthesize?text=Hello+world&lang=en" -o output_en.wav

# 話者や速度を指定
curl "http://localhost:8080/synthesize?text=テスト&lang=ja&speaker=0&length_scale=1.2" -o output_slow.wav

POSTリクエスト(JSON)での合成です。

curl -X POST http://localhost:8080/synthesize \
  -H "Content-Type: application/json" \
  -d '{"text": "こんにちは、音声合成のテストです。", "language": "ja"}' \
  -o output.wav

POSTリクエストのJSONボディは以下のフィールドを受け付けます。

{
  "text": "合成するテキスト",
  "language": "ja",
  "speaker_id": 0,
  "noise_scale": 0.667,
  "length_scale": 1.0,
  "noise_w": 0.8
}

ヘルスチェックとモデル情報の確認です。

# ヘルスチェック
curl http://localhost:8080/health
# {"status":"ok"}

# モデル情報
curl http://localhost:8080/info
# {"num_speakers":1,"num_languages":6,"languages":{"en":1,"es":4,"fr":5,"ja":0,"pt":6,"zh":2},"sample_rate":22050,...}

VoicePool: 並行セッション管理

複数のリクエストを同時に処理する場合は VoicePool を使います。database/sql.DB と同様のパターンで、Voiceインスタンスをプールして再利用します。

package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "sync"

    "github.com/ayutaz/piper-plus/src/go/piperplus"
)

func main() {
    if err := piperplus.Init(""); err != nil {
        log.Fatal(err)
    }
    defer piperplus.Shutdown()

    // 最大並行数4のプールを作成
    pool := piperplus.NewVoicePool("tsukuyomi-chan-6lang-fp16.onnx", 4)
    defer pool.Close()

    // 並行して合成リクエストを処理
    texts := []string{
        "これは1番目のリクエストです。",
        "これは2番目のリクエストです。",
        "これは3番目のリクエストです。",
        "これは4番目のリクエストです。",
        "これは5番目のリクエストです。",
    }

    var wg sync.WaitGroup
    for i, text := range texts {
        wg.Add(1)
        go func(idx int, t string) {
            defer wg.Done()

            ctx := context.Background()
            result, err := pool.Synthesize(ctx, t,
                piperplus.WithLanguage("ja"),
            )
            if err != nil {
                log.Printf("request %d failed: %v", idx, err)
                return
            }

            filename := fmt.Sprintf("output_%d.wav", idx)
            f, err := os.Create(filename)
            if err != nil {
                log.Printf("request %d: failed to create file: %v", idx, err)
                return
            }
            defer f.Close()
            result.WriteTo(f)

            fmt.Printf("request %d: RTF=%.3f\n", idx, result.RTF())
        }(i, text)
    }
    wg.Wait()
}

VoicePool の特徴は以下の通りです。

  • セマフォベースの並行制御: 指定した最大並行数を超えるリクエストはブロックされます
  • 遅延生成(lazy creation): Voiceインスタンスは必要になった時点で初めて生成されます。プール作成時にはメモリを消費しません
  • リサイクル: 使い終わったVoiceは破棄せずプールに戻して再利用します
  • goroutine安全: 複数のgoroutineから安全に呼び出せます
  • context.Context 対応: タイムアウトやキャンセルに対応しています

VoicePoolとHTTP APIサーバーを組み合わせた本番向けの構成も可能です。

pool := piperplus.NewVoicePool("model.onnx", 4)
defer pool.Close()

// プールを使った並行処理可能なHTTPハンドラ
http.HandleFunc("/synthesize", func(w http.ResponseWriter, r *http.Request) {
    text := r.URL.Query().Get("text")
    lang := r.URL.Query().Get("lang")

    result, err := pool.Synthesize(r.Context(), text,
        piperplus.WithLanguage(lang),
    )
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }

    w.Header().Set("Content-Type", "audio/wav")
    result.WriteTo(w)
})

ストリーミング合成

長いテキストをセンテンス単位で分割し、逐次的に音声を生成・送出するにはストリーミング合成を使います。

package main

import (
    "context"
    "fmt"
    "log"
    "os/exec"

    "github.com/ayutaz/piper-plus/src/go/piperplus"
)

func main() {
    if err := piperplus.Init(""); err != nil {
        log.Fatal(err)
    }
    defer piperplus.Shutdown()

    ctx := context.Background()
    voice, err := piperplus.LoadVoice(ctx, "tsukuyomi-chan-6lang-fp16.onnx")
    if err != nil {
        log.Fatal(err)
    }
    defer voice.Close()

    // aplay にパイプして逐次再生(Linux)
    cmd := exec.Command("aplay", "-r", "22050", "-f", "S16_LE", "-c", "1")
    stdin, _ := cmd.StdinPipe()
    cmd.Start()

    sink := piperplus.NewWriterAudioSink(stdin)

    text := "これはストリーミング合成のテストです。文ごとに合成されます。リアルタイムに再生できます。"
    err = voice.SynthesizeStream(ctx, text, sink,
        piperplus.WithLanguage("ja"),
    )
    if err != nil {
        log.Fatal(err)
    }

    stdin.Close()
    cmd.Wait()
    fmt.Println("ストリーミング再生完了")
}

AudioSink インターフェースは以下のように定義しています。

type AudioSink interface {
    WriteAudio(samples []int16, sampleRate int) error
    Close() error
}

SynthesizeStream() は内部でテキストをセンテンスに分割し、文ごとにONNX推論を実行してAudioSinkに送出します。隣接するチャンク間では10msのクロスフェードが適用され、境界でのクリックノイズを低減します。

CLIからもストリーミングを使えます。

# raw PCMをstdoutに出力し、aplayで再生
piper-plus -m tsukuyomi-chan-6lang-fp16.onnx -t "長いテキストをストリーミングで再生します。" --streaming | aplay -r 22050 -f S16_LE

GPU推論

WithDevice() オプションでGPU推論を有効にできます。

// CUDA(デフォルトGPU)
voice, err := piperplus.LoadVoice(ctx, "model.onnx",
    piperplus.WithDevice("cuda"),
)

// CUDA(特定のGPU)
voice, err := piperplus.LoadVoice(ctx, "model.onnx",
    piperplus.WithDevice("cuda:1"),
)

// CoreML(macOS)
voice, err := piperplus.LoadVoice(ctx, "model.onnx",
    piperplus.WithDevice("coreml"),
)

// DirectML(Windows)
voice, err := piperplus.LoadVoice(ctx, "model.onnx",
    piperplus.WithDevice("directml"),
)

// 自動検出(CUDA → CoreML → DirectML → CPU の順にフォールバック)
voice, err := piperplus.LoadVoice(ctx, "model.onnx",
    piperplus.WithDevice("auto"),
)
デバイス 説明
cpu CPU推論(デフォルト)
cuda / cuda:N NVIDIA CUDA
coreml Apple CoreML(macOS)
directml / directml:N Microsoft DirectML(Windows)
tensorrt / tensorrt:N NVIDIA TensorRT
auto 利用可能なGPUを自動検出、失敗時はCPUにフォールバック

CLIでも --device フラグで指定できます。

piper-plus -m model.onnx -t "CUDAで合成" --device cuda -f output.wav

Dockerデプロイ

Go SDKはマルチステージビルドのDockerfile(src/go/docker/Dockerfile)を提供しています。ビルドステージでOpenJTalkをソースからCMakeビルドし、日本語G2Pを有効化した状態でGoバイナリをコンパイルします。ランタイムはDebian(trixie-slim)ベースで、ONNX Runtime v1.24.4とOpenJTalk辞書を同梱しています。

# イメージのビルド
docker build -t piper-plus-go -f src/go/docker/Dockerfile .

# CLIモードで実行
docker run --rm -v ./models:/models:ro \
  piper-plus-go -m /models/tsukuyomi-chan-6lang-fp16.onnx \
  -t "Dockerで合成テスト" --language ja -f /dev/stdout > output.wav

# HTTPサーバーとして起動
docker run -p 8080:8080 -v ./models:/models:ro \
  piper-plus-go serve -m /models/tsukuyomi-chan-6lang-fp16.onnx --addr :8080

CGO_ENABLED=1が必要な点に注意してください。これはONNX Runtimeのバインディングに加え、日本語G2P(OpenJTalk)のネイティブライブラリにも必要です。CGOが無効な場合、ビルドは失敗します。

実行結果

基本的な合成プログラムをDocker(CPU推論)で実行した結果です。

$ piper-plus -m tsukuyomi-chan-6lang-fp16.onnx -t "こんにちは、今日は良い天気ですね。" --language ja -f output.wav
time=2026-04-09T05:40:24.821Z level=INFO msg="voice loaded" model=tsukuyomi-chan-6lang-fp16.onnx device=cpu
time=2026-04-09T05:40:24.998Z level=INFO msg=synthesized duration=1.95047619s infer_time=149.725369ms rtf=0.077

CPU推論でRTF 0.077、約1.95秒の音声を約150msで合成できています(実時間の約13倍の速度)。

HTTPサーバーを起動し、curlでリクエストした結果です。

$ curl http://localhost:8080/health
{"status":"ok"}

$ curl http://localhost:8080/info
{"num_speakers":1,"num_languages":6,"languages":{"en":1,"es":3,"fr":4,"ja":0,"pt":5,"zh":2},"capabilities":{"HasSpeakerID":false,"HasLanguageID":true,"HasProsody":true,"HasDurationOutput":true},"sample_rate":22050}

$ curl "http://localhost:8080/synthesize?text=こんにちは&lang=ja" -o output.wav
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 29740    0 29740    0     0   232k      0 --:--:--  0:00:00 --:--:--  232k

合成された音声ファイルは22050Hz、16-bit モノラルのWAV形式で出力されます。

所感

Goの利点がTTSサーバーの用途にうまく合っています。シングルバイナリでのデプロイ、goroutineによる並行処理、context.Contextによるタイムアウト管理が自然に使えます。VoicePoolはリクエスト数が増えた場合の並行制御に有効で、database/sql.DBと同様の使い勝手でVoiceインスタンスを管理できます。ONNX Runtimeの共有ライブラリが必要な点はPure Goとは言えませんが、Dockerでパッケージングすればデプロイの問題にはなりません。

piper-plus-g2p: TTS不要で使えるMITライセンスの多言語G2Pパッケージ

初めに

今回は、piper-plusから独立したG2P(Grapheme-to-Phoneme、文字から音素への変換)パッケージ「piper-plus-g2p」を紹介します。

piper-plus-g2pは、TTSエンジンを使わずに8言語(ja, en, zh, ko, es, fr, pt, sv)のテキストをIPA音素列に変換できるパッケージです。このうちTTSモデルが公開済みなのは6言語(ja, en, zh, es, fr, pt)ですが、G2P単体としては8言語すべてに対応しています。従来のpiper-plusではeSpeak-ng(GPLライセンス)に依存していましたが、piper-plus-g2pはeSpeak-ngを完全に排除し、MITライセンスで利用できます。PR #300(2026-04-01)で追加し、2026-04-07にPyPI(v0.2.0)およびcrates.io(v0.2.0)で公開しました。

設計方針として「IPA-first」を採用しており、phonemize() はBOS/EOS/PUAマーカーを含まない純粋なIPAトークン列を返します。Piper互換のphoneme_idsが必要な場合はオプションの PiperEncoder を使います。

Python(PyPI: v0.2.0)、npm(@piper-plus/g2p: v0.2.0)、Rust(crates.io: v0.2.0)、Goの4つのSDKを提供しており、4実装間でPUA互換テーブル(96エントリ)をCIで一致検証しています。

github.com

開発環境

  • OS: Windows 11
  • Python: 3.12
  • パッケージマネージャー: uv
  • piper-plus-g2p: 0.2.0

環境構築

全言語をインストールする場合

uv init piper-g2p-demo
cd piper-g2p-demo
uv add "piper-plus-g2p[all]>=0.2.0"

日本語のみの場合

uv add "piper-plus-g2p[ja]"

個別言語のextras

必要な言語だけをインストールすることで依存関係を最小限にできます。

extra 言語 主な依存 備考
[ja] 日本語 pyopenjtalk-plus
[en] 英語 CMU Dictionary + g2p-en
[zh] 中国語 ピンイン辞書
[ko] 韓国語 g2pk2 (MeCab/eunjeon) Windows環境ではMeCabのインストールに問題が発生する場合あり
[es] スペイン語 ルールベース
[fr] フランス語 ルールベース
[pt] ポルトガル語 ルールベース
[sv] スウェーデン語 NST辞書

複数言語を組み合わせる場合は以下のように指定します。

uv add "piper-plus-g2p[ja,en,zh]"

基本的な使い方

言語ごとのPhonemizer取得

get_phonemizer() に言語コードを渡すと、対応するPhonemizerインスタンスが返されます。

from piper_plus_g2p import get_phonemizer

# 日本語のPhonemizer
ja_phonemizer = get_phonemizer("ja")

# テキストをIPA音素列に変換
result = ja_phonemizer.phonemize("こんにちは")
print(result)
['k', 'o', '[', 'N_n', 'n', 'i', 'ch', 'i', 'w', 'a']

各言語の出力例

from piper_plus_g2p import get_phonemizer

# 日本語
ja = get_phonemizer("ja")
print("ja:", ja.phonemize("こんにちは"))

# 英語
en = get_phonemizer("en")
print("en:", en.phonemize("Hello, world!"))

# 中国語
zh = get_phonemizer("zh")
print("zh:", zh.phonemize("你好世界"))
ja: ['k', 'o', '[', 'N_n', 'n', 'i', 'ch', 'i', 'w', 'a']
en: ['h', 'ə', 'l', 'ˈ', 'o', 'ʊ', ',', ' ', 'w', 'ˈ', 'ɜ', 'ː', 'l', 'd', '!']
zh: ['n', 'i', 'tone2', 'x', 'aʊ', 'tone3', 'ʂ', 'ɻ̩', 'tone4', 'tɕ', 'iɛ', 'tone4']

注意: 韓国語(ko)のG2Pはg2pk2を使用しており、内部でMeCab(eunjeon)を必要とします。Windows環境ではMeCabのインストールに問題が発生する場合があるため、上記の例からは除外しています。Linux/macOS環境であれば get_phonemizer("ko") で利用可能です。

IPA-first設計のため、出力にはBOS(^)やEOS($)などのマーカーは含まれません。

日本語の韻律情報(ProsodyInfo)

日本語Phonemizerでは、音素列に加えて韻律情報(アクセント・イントネーション)も取得できます。

from piper_plus_g2p import get_phonemizer

ja = get_phonemizer("ja")
tokens, prosody = ja.phonemize_with_prosody("こんにちは")
print("prosody:", prosody[0])
prosody: ProsodyInfo(a1=-4, a2=1, a3=5)

OpenJTalkのアクセント情報(a1, a2, a3)がそのまま取得でき、TTS学習時のアクセント制御に利用できます。

PiperEncoderでphoneme_idsに変換(オプション)

Piper互換のphoneme_idsが必要な場合は PiperEncoder を使います。

from piper_plus_g2p import get_phonemizer, PiperEncoder
import json

ja = get_phonemizer("ja")
phonemes = ja.phonemize("こんにちは")

# config.jsonからphoneme_id_mapを読み込み
with open("config.json") as f:
    config = json.load(f)

encoder = PiperEncoder(config["phoneme_id_map"])
phoneme_ids = encoder.encode(phonemes)
print(phoneme_ids)

PiperEncoder はコンストラクタに phoneme_id_map(モデルの config.json に含まれる音素→IDのマッピング辞書)が必要です。BOS(ID: 1)とEOS(ID: 2)は自動付加されます。

多言語テキストの自動言語検出

MultilingualPhonemizer

日本語と英語が混在するテキストを処理する場合は、get_phonemizer() にハイフン区切りの複合言語コード(例: "ja-en-zh")を渡します。内部で MultilingualPhonemizer が自動的に作成され、Unicodeブロックに基づく言語検出で混在テキストを処理します。

from piper_plus_g2p import get_phonemizer

# ハイフン区切りの複合言語コードを渡すとMultilingualPhonemizerが自動生成される
phonemizer = get_phonemizer("ja-en")

# 日本語・英語混在テキスト
result = phonemizer.phonemize("Dockerコンテナを起動する")
print(result)

言語検出はUnicodeブロックに基づいて自動判定されます。

文字種 判定言語
カナ文字を含む 日本語 (ja)
CJK文字(カナなし) 中国語 (zh)
ハングル 韓国語 (ko)
ラテン文字 英語 (en)

カスタム辞書サポート

特定の単語の読みをカスタマイズする場合、CustomDictionary クラスを使います。apply_to_text() でテキスト内の単語を辞書の読みに置換し、その後にG2Pで音素変換する流れです。

from piper_plus_g2p import get_phonemizer
from piper_plus_g2p.custom_dict import CustomDictionary

# カスタム辞書を作成し、単語を追加
d = CustomDictionary()
d.add_word("Docker", "ドッカー", priority=10)
d.add_word("piper", "パイパー", priority=10)

# テキストに辞書を適用してから音素変換
text = "Dockerコンテナを起動する"
text = d.apply_to_text(text)  # "ドッカーコンテナを起動する"

ja = get_phonemizer("ja")
result = ja.phonemize(text)
print(result)

JSON辞書ファイルからの読み込みにも対応しています。

d = CustomDictionary(dict_paths="my_dict.json")

他のSDKについて

piper-plus-g2pはPython以外にも複数のSDKを提供しています。

npm (@piper-plus/g2p: v0.2.0)

npm install @piper-plus/g2p
import { G2P, DictLoader } from "@piper-plus/g2p";

// 日本語にはOpenJTalk辞書の読み込みが必要
const loader = new DictLoader();
const jaDict = await loader.loadJaDict();
const g2p = await G2P.create({ languages: ["ja", "en"], jaDict });

const result = g2p.phonemize("こんにちは");
console.log(result.tokens);   // ['k', 'o', ...]
console.log(result.language);  // 'ja'

g2p.dispose();

G2P.create() は非同期ファクトリで、日本語はOpenJTalk WASM + 辞書データの初期化が必要です。DictLoader が辞書のダウンロードとIndexedDBキャッシュを管理します。phonemize(){ tokens, prosody, language } を返します。韻律情報が必要な場合は phonemizeWithProsody() を使います。380テストでカバーしています。

Rust (crates.io: v0.2.0)

cargo add piper-plus-g2p@0.2.0 --features naist-jdic
use piper_plus_g2p::Phonemizer;
use piper_plus_g2p::japanese::JapanesePhonemizer;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let ja = JapanesePhonemizer::new_bundled()?;
    let (tokens, prosody) = ja.phonemize_with_prosody("こんにちは")?;
    println!("{:?}", tokens);
    Ok(())
}

PhonemizerRegistry を使って複数言語をまとめて管理することもできます。Feature flagsで言語ごとの依存を制御可能です(features = ["naist-jdic", "english"])。390テストでカバーしています。

Go

Go SDKのG2Pはモノレポ内のモジュールとして提供しています。

import "github.com/ayutaz/piper-plus/src/go/phonemize"

// 各言語のPhonemizerを個別に作成
esPhonemizer := phonemize.NewSpanishPhonemizer()
result, err := esPhonemizer.PhonemizeWithProsody("Hola, mundo")

日本語G2PはCGO + OpenJTalkが必要です。詳細はGo APIサーバーの記事を参照してください。

4実装間の互換性保証

Python、npm、Rust、Goの4実装は、PUA互換テーブル(96エントリ)をCIで一致検証しています。どのSDKを使っても同じテキストに対して同じ音素列が生成されます。

所感

piper-plus-g2pはTTSを使わずに音素変換だけを利用したいユースケース(発音辞書生成、語学アプリ、音声検索のインデックス構築など)に適しています。eSpeak-ng(GPL)を排除してMITライセンスで8言語のG2Pが使える点は、商用プロダクトへの組み込みにも有利です。4つのSDKでCIレベルの互換性が保証されている点も実用的です。

piper-plus v1.11.0のRust SDKで高速TTSを実現する

初めに

今回は、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 で即座に使い始められます。

github.com

開発環境

  • 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))?;

SynthesisRequestSynthesisResult の構造は以下の通りです。

/// 合成パラメータ(低レベル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

フォネムタイミングはリップシンク同期、字幕生成、カラオケ表示などに活用できます。PhonemeTimingInfoTimingResult の構造は以下の通りです。

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 installcargo add でのセットアップが格段に簡単になりました。

piper-plusのDockerイメージでTTSのWebUIを提供する

初めに

今回は、piper-plusのDockerイメージを使って、ブラウザ上で操作できるGradio WebUIのTTS環境を構築します。

piper-plusはDockerイメージ群を提供しており、CLI推論・FastAPIサーバー・Gradio WebUIの3つの利用モードに対応しています。PIPER_MODEL 環境変数を指定するだけで、起動時にモデルが自動ダウンロードされるため、Dockerさえあれば他の環境構築は不要です。

github.com

開発環境

  • OS: Linux推奨(Docker Desktop経由でWindows/macOSも可)
  • Docker: 24.x以上
  • Docker Compose: v2
  • (オプション)NVIDIA GPU + nvidia-container-toolkit

GPU推論を使う場合は、ホストマシンにNVIDIA GPU用のドライバとnvidia-container-toolkitがインストールされている必要があります。CPU推論のみであれば不要です。

Dockerイメージの構成

piper-plusは用途別に4つのDockerイメージを提供しています。

イメージ 用途 説明
webui Gradio WebUI ブラウザ上でTTSを操作できるUI。CPUのみ
python-inference Python推論 CLI/FastAPIサーバー。CPU/GPU切替対応
python-train モデル学習 PyTorch Lightningベースの学習パイプライン
cpp-inference C++推論 軽量なC++バイナリによる推論

この記事では主にwebuiイメージの使い方を紹介します。

Gradio WebUIの起動

リポジトリのクローンとビルド

まず、piper-plusのリポジトリをクローンし、webuiイメージをビルドします。

git clone https://github.com/ayutaz/piper-plus.git
cd piper-plus
docker build -t piper-webui -f docker/webui/Dockerfile .

コンテナの起動(モデル自動ダウンロード)

PIPER_MODEL 環境変数にモデル名を指定すると、起動時にモデルが自動でダウンロードされます。ダウンロードされたモデルはボリュームマウントした models/ ディレクトリに保存され、2回目以降はキャッシュから読み込まれます。

mkdir -p models
docker run -it -p 7860:7860 -e PIPER_MODEL=css10 -v ./models:/models piper-webui

初回起動時のログ:

Checking model: css10
Downloading model: css10...
Model ready: /models/css10-ja-6lang-fp16.onnx
* Running on local URL:  http://0.0.0.0:7860

既にモデルがダウンロード済みの場合はダウンロードがスキップされ、即座にWebUIが起動します。

ブラウザでアクセス

コンテナが起動したら、ブラウザで以下のURLにアクセスします。

http://localhost:7860

Gradio WebUIが表示されます。画面には以下の要素があります。

  • テキスト入力欄: 合成したいテキストを入力
  • 言語選択: ja / en / zh / ko / es / fr / pt / sv から選択
  • モデル選択: マウントされたモデルから選択
  • パラメータ調整: 発話速度(length_scale)、ランダム性(noise_scale)、音素長ばらつき(noise_w)
  • 合成ボタン: クリックで音声合成を実行
  • 音声プレイヤー: 合成結果を再生・ダウンロード

手動でモデルを配置する場合

PIPER_MODEL を指定せずに、事前にダウンロードしたモデルをマウントすることもできます。

docker run -it -p 7860:7860 -v ./models:/models:ro piper-webui

models/ ディレクトリにはONNXモデルファイルとconfig.jsonを配置します。

models/
├── css10-ja-6lang-fp16.onnx
└── config.json

docker-composeによる起動

リポジトリにはdocker-compose.ymlが付属しています。

cd docker/webui

PIPER_MODEL 環境変数でダウンロードするモデルを指定して起動します。

PIPER_MODEL=css10 docker compose up

docker-compose.ymlの内容:

version: '3.8'

services:
  piper-webui:
    build:
      context: ../..
      dockerfile: docker/webui/Dockerfile
    container_name: piper-webui
    ports:
      - "7860:7860"
    volumes:
      - ${MODELS_DIR:-./models}:/models
      - ${OUTPUT_DIR:-./output}:/output
    environment:
      - PIPER_MODEL=${PIPER_MODEL:-}
      - PIPER_MODEL_DIR=/models
      - GRADIO_SERVER_NAME=0.0.0.0
      - GRADIO_SERVER_PORT=7860
      - PYTHONUNBUFFERED=1
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:7860/')"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

ヘルスチェックも設定されているため、コンテナの状態を docker ps で確認できます。

複数モデルの利用

複数のモデルを配置する場合は、models/ ディレクトリに並べて配置します。WebUI上でモデルを切り替えて使用できます。

models/
├── css10-ja-6lang-fp16.onnx
├── config.json
└── tsukuyomi/                    # オプション
    ├── tsukuyomi-chan-6lang-fp16.onnx
    └── config.json

つくよみちゃんモデルなど追加のモデルをダウンロードする場合:

docker run --rm -e PIPER_MODEL=tsukuyomi -v ./models:/models piper-webui echo "done"

所感

PIPER_MODEL 環境変数を指定するだけでモデルの自動ダウンロードからWebUIの起動まで完結します。Dockerさえあれば、PythonやONNX Runtimeのセットアップは不要です。ボリュームマウントによりモデルがホスト側にキャッシュされるため、2回目以降は即座に起動します。

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で自分の音声データを使った追加学習(ファインチューニング)を試みます。

piper-plusのPython SDKを使って日本語TTSの推論をしてみる

初めに

今回は、自分が開発しているTTSライブラリ「piper-plus」のPython SDKを使って、日本語の音声合成(Text-to-Speech)を試してみます。

piper-plusはVITSアーキテクチャをベースにした多言語対応の高速TTSシステムで、日本語・英語・中国語・韓国語など8言語に対応しています。Python、npm、C#、Rust、Go、C++、WASMなど多くのプラットフォーム向けSDKを提供していますが、この記事ではPython SDKでの基本的な使い方を紹介します。

github.com

開発環境

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

環境構築

プロジェクトの作成

まず、uvでプロジェクトを作成します。

uv init piper-plus-python-demo
cd piper-plus-python-demo

インストール

CPU推論の場合は以下のコマンドでインストールします。

uv add piper-plus

GPU(CUDA)を使って推論したい場合は、inference-gpu extraを指定します。

uv add "piper-plus[inference-gpu]"

日本語モデルのダウンロード

piper-plusにはモデルのダウンロード機能が組み込まれています。利用可能なモデルの一覧は以下のコマンドで確認できます。

uv run python -m piper --list-models ja
Available voice models:

  Japanese (日本語) [ja_JP]:
    ja_JP-css10-6lang-medium                [piper-plus]  1 speaker   medium
    ja_JP-tsukuyomi-chan-medium             [piper-plus]  1 speaker   medium

Use --download-model <name> to download a model.

今回はつくよみちゃんコーパスで学習されたモデルをダウンロードします。

uv run python -m piper --download-model tsukuyomi

ダウンロードが完了すると、以下の2ファイルが配置されます。

  • tsukuyomi-chan-6lang-fp16.onnx — ONNXフォーマットのモデル本体(FP16)
  • config.json — 音素マッピングや音声設定などの構成ファイル

ファイルはカレントディレクトリに配置されます。

基本的な推論

Pythonコードからの推論

PiperVoice クラスを使ってテキストからWAVファイルを生成します。

import wave
from piper import PiperVoice

# モデルの読み込み
voice = PiperVoice.load("tsukuyomi-chan-6lang-fp16.onnx", config_path="config.json", use_cuda=False)

# テキストからWAVファイルを生成
with wave.open("output.wav", "wb") as wav_file:
    voice.synthesize("こんにちは、今日はいい天気ですね。", wav_file)

synthesize() メソッドには以下のパラメータを指定できます。

パラメータ デフォルト 説明
length_scale 1.0 発話速度。小さいほど速くなる
noise_scale 0.667 音声のランダム性。大きいほど表現が多様になる
noise_w 0.8 音素の長さのばらつき
volume 1.0 音量
speaker_id 0 話者ID(マルチスピーカーモデルの場合)
language_id 0 言語ID(ja=0, en=1, zh=2, ko=3, es=4, fr=5, pt=6, sv=7)

パラメータを指定する例です。

with wave.open("output_slow.wav", "wb") as wav_file:
    voice.synthesize(
        "ゆっくり話してみます。",
        wav_file,
        length_scale=1.3,   # ゆっくり
        noise_scale=0.5,    # 安定した音声
        noise_w=0.6,
        volume=0.8
    )

GPU推論

GPU推論を有効にする場合は use_cuda=True を指定します。

voice = PiperVoice.load("tsukuyomi-chan-6lang-fp16.onnx", config_path="config.json", use_cuda=True)

CLIからの推論

Pythonコードを書かずに、コマンドラインから直接推論することもできます。テキストは位置引数として渡します。

uv run python -m piper -m tsukuyomi-chan-6lang-fp16.onnx -f output.wav "こんにちは、音声合成のテストです。"

GPU推論を使う場合は --cuda フラグを追加します。

uv run python -m piper -m tsukuyomi-chan-6lang-fp16.onnx -f output.wav --cuda "こんにちは"

生成した音声をすぐに再生したい場合は --auto-play フラグが便利です。

uv run python -m piper -m tsukuyomi-chan-6lang-fp16.onnx --auto-play "再生テスト"

ストリーミング合成

長いテキストを文ごとに分割してリアルタイムに音声生成する場合は、synthesize_stream_raw() を使います。

import wave
import io
from piper import PiperVoice

voice = PiperVoice.load("tsukuyomi-chan-6lang-fp16.onnx", config_path="config.json")

text = "これはストリーミング合成のテストです。文ごとにチャンクが生成されます。リアルタイムに再生できます。"

# 文ごとにraw PCMデータがyieldされる
with wave.open("stream_output.wav", "wb") as wav_file:
    wav_file.setnchannels(1)
    wav_file.setsampwidth(2)
    wav_file.setframerate(voice.config.sample_rate)

    for audio_chunk in voice.synthesize_stream_raw(text):
        wav_file.writeframes(audio_chunk)

synthesize_stream_raw() は文の区切りごとにraw PCMバイト列をyieldするため、再生ライブラリと組み合わせればリアルタイム再生が可能です。

カスタム辞書と直接フォネム入力

特定の単語の読みを制御したい場合は、[[ ]] 記法でフォネムを直接指定できます。

# 「piper」を「パイパー」と読ませる
text = "[[p a i p a a]]のテストです。"

with wave.open("phoneme_output.wav", "wb") as wav_file:
    voice.synthesize(text, wav_file)

固有名詞や専門用語の読みを正確に制御したい場合に便利です。

所感

piper-plusのPython SDKはインストールからモデルダウンロード、推論まで数行のコードで完結します。GPU推論を使えばリアルタイム以上の速度で音声合成が可能です。次回はnpmパッケージを使ってブラウザ上でTTSを動かしてみます。

日本語を含む6言語対応の軽量TTS「piper-plus」をバイナリファイルから実行して推論する

初めに

スマホやIoTでも動かすことができる日本語を含む6言語対応の軽量TTSのpiper-plusを公開しています 学習も自由にできるようにOSSとして公開しています

github.com

piper-plusはC#,Rust,C++の3言語のバイナリビルドを行っていて任意のファイルから実行することができます

開発環境

  • Windows 11

バイナリのダウンロードと確認

まずは以下のリリースノートからバイナリをダウンロードします

github.com

どれでもほぼ同じなので、今回はC++のバイナリファイルを使って推論をしてみます

ダウンロードして解凍すると以下のような構造になっています

 piper-windows-x64/
  └── piper/
      ├── bin/
      │   ├── piper.exe
      │   ├── onnxruntime.dll
      │   ├── onnxruntime_providers_shared.dll
      │   ├── concrt140.dll
      │   ├── msvcp140.dll
      │   ├── msvcp140_1.dll
      │   ├── msvcp140_2.dll
      │   ├── msvcp140_atomic_wait.dll
      │   ├── msvcp140_codecvt_ids.dll
      │   ├── ucrtbase.dll
      │   ├── vcruntime140.dll
      │   ├── vcruntime140_1.dll
      │   └── api-ms-win-*.dll (多数のWindows API DLL)
      ├── lib/
      │   ()
      └── share/
          ├── open_jtalk/
          │   └── dic/
          │       ├── COPYING
          │       ├── char.bin
          │       ├── left-id.def
          │       ├── matrix.bin
          │       ├── pos-id.def
          │       ├── rewrite.def
          │       ├── right-id.def
          │       ├── sys.dic
          │       └── unk.dic
          └── piper/
              └── dicts/
                  ├── cmudict_data.json
                  ├── pinyin_phrases.json
                  └── pinyin_single.json

次にpiperフォルダで PowerShellを開きます

以下のコマンドを実行するとコマンドのヘルプが確認できます

.\bin\piper.exe --help

以下の出力がでます

options:
   -h        --help              show this message and exit
   -m  FILE  --model       FILE  path to onnx model file
   -c  FILE  --config      FILE  path to model config file (default: model path + .json, fallback: config.json in model dir)
   -t  TEXT  --text        TEXT  text to synthesize (no stdin required)
   -f  FILE  --output_file FILE  path to output WAV file ('-' for stdout)
   -d  DIR   --output_dir  DIR   path to output directory (default: cwd)
   --output_raw                  output raw audio to stdout as it becomes available
   -s  NUM   --speaker     NUM   id of speaker (default: 0)
   -l  CODE  --language    CODE  language code or id (default: auto)
   --noise_scale           NUM   generator noise (default: 0.667)
   --length_scale          NUM   phoneme length (default: 1.0)
   --noise_w               NUM   phoneme width noise (default: 0.8)
   --sentence_silence      NUM   seconds of silence after each sentence (default: 0.2)
   --phoneme_silence <phoneme> <seconds>  Set silence for a specific phoneme
   --custom-dict       FILE       path to custom dictionary file(s), comma-separated

   Phoneme input: Use [[ phonemes ]] notation to specify exact pronunciation
                  Example: echo "Hello [[ h ə l oʊ ]] world" | piper ...

   --json-input                  stdin input is lines of JSON instead of plain text
   --use-cuda                    use CUDA execution provider
   --gpu-device-id         NUM   GPU device ID for CUDA (default: 0)
   --raw-phonemes                interpret input as raw phonemes (space-separated)
   --streaming                   use streaming mode for reduced latency
   --output-timing         FILE  output phoneme timing to FILE
   --timing-format         FMT   timing output format: json|tsv (default: json)
   --list-models      [LANG]     list available voice models
   --download-model   NAME       download a voice model
   --model-dir        DIR        directory for downloaded models

   --debug                       print DEBUG messages to the console
   -q       --quiet              disable logging

environment variables:
   PIPER_DEFAULT_MODEL           default model path (if --model not specified)
   PIPER_DEFAULT_CONFIG          default config file path
   PIPER_MODEL_DIR               default model directory (if --model-dir not specified)
   PIPER_GPU_DEVICE_ID           GPU device ID for CUDA

モデルのダウンロード

次に公式が提供してるモデルを確認します

 .\bin\piper.exe --list-models

このコマンドを実行すると以下のように公式が提供しているCSS10(1)とつくよみちゃんモデル(2) があることがわかります

Available voice models:

  Japanese (日本語) [ja_JP]:
    ja_JP-css10-6lang-medium                [piper-plus]  1 speaker   medium   (css10, css10-6lang, css10-ja, ja-css10)
    ja_JP-tsukuyomi-chan-medium             [piper-plus]  1 speaker   medium   (tsukuyomi, tsukuyomi-chan, ja-tsukuyomi)

Use --download-model <name> to download a model.

今回はつくよみちゃんモデルを使うため、以下のコマンドでモデルのダウンロードを行います

.\bin\piper.exe --download-model tsukuyomi

実行すると以下のようにモデルのダウンロードが実行されます

[2026-03-22 19:32:39.945] [piper] [info] Downloading model: ja_JP-tsukuyomi-chan-medium (piper-plus)
[2026-03-22 19:32:39.947] [piper] [info]   config.json already exists, skipping
[2026-03-22 19:32:39.948] [piper] [info]   tsukuyomi-chan-6lang-fp16.onnx already exists, skipping

Model downloaded successfully!
Use with:  --model C:\Users\{username}\AppData\Roaming\piper\models\tsukuyomi-chan-6lang-fp16.onnx

(*1,2)

それぞれ以下のhuggingfaceで公開しています

huggingface.co

huggingface.co

推論

次に音声合成を実行します

コマンドは以下になります

\bin\piper.exe --model tsukuyomi --text "こんにちは、今日は良い天気ですね。"

--output_file output.wav をつけることでファイル名を指定できます