piper-plusのWASM版でブラウザ上の日本語TTSを実現する

初めに

今回は、piper-plusのnpmパッケージ(WASM版)を使ったブラウザ上での日本語音声合成について紹介します。サーバー不要でクライアントサイドのみで動作します。

piper-plusはWebAssemblyにも対応しており、OpenJTalk辞書を内蔵したWASMバイナリ(約60MB、gzip転送時約19MB)とJSバインディングで構成されています。モデルの読み込みから音声合成まですべてクライアントサイドで完結するため、サーバーの構築が不要です。

github.com

デモページも公開しています。

ayutaz.github.io

開発環境

  • OS: Windows 11
  • Node.js: 22.x
  • ブラウザ: Google Chrome
  • パッケージマネージャー: npm
  • piper-plus (npm): 0.3.1

環境構築

プロジェクトの作成

新しいプロジェクトを作成します。

mkdir piper-plus-web-demo
cd piper-plus-web-demo
npm init -y

パッケージのインストール

piper-plusと、peer dependencyのonnxruntime-webをインストールします。開発サーバーとしてViteを使用します。

npm install piper-plus onnxruntime-web
npm install -D vite

onnxruntime-web はバージョン1.21.0以上が必要です。

package.jsonの設定

ESモジュールを使用するため、package.json"type": "module" と開発サーバーの起動スクリプトを追加します。

{
  "name": "piper-plus-web-demo",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite"
  },
  "dependencies": {
    "onnxruntime-web": "^1.24.3",
    "piper-plus": "^0.3.1"
  },
  "devDependencies": {
    "vite": "^8.0.8"
  }
}

Vite設定ファイルの作成(vite.config.js)

piper-plusはWASMバイナリを相対パスで読み込むため、Viteのdependency pre-bundlingから除外する必要があります。プロジェクトルートに vite.config.js を作成します。

import { defineConfig } from "vite";

export default defineConfig({
  optimizeDeps: {
    exclude: ["piper-plus"],
  },
});

この設定がないと、WASMファイルのパス解決が壊れて日本語の音声合成が動作しません。

モデルの準備

piper-plusのnpmパッケージは、HuggingFaceからモデルを自動ダウンロードする仕組みになっています。PiperPlus.initialize() にHuggingFaceのリポジトリ名を指定するだけでモデルが取得されます。

今回はつくよみちゃんモデルを使用します。

huggingface.co

ダウンロードされたONNXモデルとconfigはブラウザのIndexedDB(piper-plus-models)にキャッシュされるため、2回目以降のアクセスではダウンロードが発生しません。OpenJTalkの辞書はWASMバイナリに埋め込まれているため、個別のダウンロードは不要です。

実装

最小限のHTML(index.html)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>piper-plus Web TTS Demo</title>
</head>
<body>
    <h1>piper-plus Web TTS Demo</h1>
    <textarea id="text" rows="3" cols="50">こんにちは、今日はいい天気ですね。</textarea>
    <br>
    <button id="synthesize" disabled>音声合成</button>
    <p id="status">モデルを読み込み中...</p>
    <script type="module" src="main.js"></script>
</body>
</html>

JavaScript(main.js)

index.html<script> タグで参照している main.js を作成します。

import { PiperPlus } from "piper-plus";
import * as ort from "onnxruntime-web";

const statusEl = document.getElementById("status");
const button = document.getElementById("synthesize");
const textArea = document.getElementById("text");

// モデルの初期化
const tts = await PiperPlus.initialize({
    model: "ayousanz/piper-plus-tsukuyomi-chan",
    ort,
    onProgress: ({ stage, progress, message }) => {
        statusEl.textContent = `${stage}: ${Math.round(progress * 100)}% - ${message}`;
    },
});

statusEl.textContent = "準備完了";
button.disabled = false;

// 音声合成ボタンのイベント
button.addEventListener("click", async () => {
    button.disabled = true;
    statusEl.textContent = "合成中...";

    const text = textArea.value;
    const audio = await tts.synthesize(text, { language: "ja" });

    // ブラウザで再生
    await audio.play();

    statusEl.textContent = "再生完了";
    button.disabled = false;
});

ここまでで、プロジェクトのファイル構成は以下のようになります。

piper-plus-web-demo/
├── index.html
├── main.js
├── vite.config.js
├── package.json
└── node_modules/

開発サーバーの起動

開発サーバーを起動します。

npm run dev
  VITE v8.0.8  ready in 200 ms

  ➜  Local:   http://localhost:5173/

ブラウザで http://localhost:5173/ を開くと、初回はモデルのダウンロードが始まります(約35MB、IndexedDBにキャッシュされるため2回目以降は不要)。「準備完了」と表示されたら、テキストを入力して「音声合成」ボタンを押すと音声が再生されます。

実行結果

ブラウザでの動作結果です。デモページでも同じ動作を確認できます。

ayutaz.github.io

APIの詳細

ここからは、piper-plusのnpmパッケージが提供するAPIについて詳しく紹介します。

PiperPlus.initialize() のオプション

オプション 説明
model string 必須。HuggingFaceリポジトリ名、ショートカット("tsukuyomi")、またはONNXのURL
ort object onnxruntime-webモジュール。省略時は globalThis.ort を使用
onProgress function { stage, progress, message } を受け取るコールバック

合成結果の利用

synthesize() が返すaudioオブジェクトには複数の出力形式を用意しています。

const audio = await tts.synthesize("テスト", { language: "ja" });

// ブラウザで再生
await audio.play();

// WAV形式のArrayBuffer
const wavBuffer = audio.toWav();

// Blob形式(audio/wav)
const blob = audio.toBlob();

// ファイルダウンロード
audio.download("output.wav");

// 生のサンプルデータ
console.log(audio.samples);     // Float32Array
console.log(audio.sampleRate);  // 22050
console.log(audio.duration);    // 秒数

合成パラメータ

synthesize() には言語指定以外にも音声の品質を調整するパラメータがあります。

const audio = await tts.synthesize("パラメータ調整のテストです。", {
    language: "ja",
    noiseScale: 0.5,    // 音声のランダム性(デフォルト: 0.667)
    lengthScale: 1.3,   // 発話速度。大きいほどゆっくり(デフォルト: 1.0)
    noiseW: 0.6,        // 音素の長さのばらつき(デフォルト: 0.8)
});
パラメータ デフォルト 説明
language 自動判定 言語コード('ja', 'en', 'zh', 'ko', 'es', 'fr', 'pt', 'sv'
noiseScale 0.667 音声のランダム性。大きいほど表現が多様になる
lengthScale 1.0 発話速度。小さいほど速くなる
noiseW 0.8 音素の長さのばらつき

ストリーミング合成

長いテキストを文ごとにリアルタイム合成する場合は synthesizeStreaming() を使います。

await tts.synthesizeStreaming("長いテキストです。文ごとに合成されます。逐次的に再生できます。", {
    language: "ja",
    onChunk: (pcmFloat32Array) => {
        // 文ごとにPCMデータが返される
        // Web Audio APIなどで逐次再生可能
        console.log("チャンク受信:", pcmFloat32Array.length, "サンプル");
    },
});

言語の自動判定

piper-plusのnpmパッケージはテキストの文字種から言語を自動判定します。

文字種 判定言語
カナ文字(ひらがな・カタカナ) 日本語 (ja)
ハングル 韓国語 (ko)
CJK漢字(カナあり) 日本語 (ja)
CJK漢字(カナなし) 中国語 (zh)
スウェーデン語固有文字(å/ä/ö) スウェーデン語 (sv)
ラテン文字 英語 (en)(デフォルト)

ハングルは韓国語(ko)、スウェーデン語固有文字(å/ä/ö等)はスウェーデン語(sv)として判定されます。ただし、ラテン文字のみのテキストではスペイン語(es)、フランス語(fr)、ポルトガル語(pt)、スウェーデン語(sv)の区別ができず、デフォルトで英語(en)と判定されます。これらの言語を使用する場合は language オプションで明示的に指定してください。

明示的に言語を指定することもできます。

const audio = await tts.synthesize("Hello, world!", { language: "en" });

リソースの解放

使い終わったら dispose() でリソースを解放します。ONNXセッションやWASMモジュールが解放されます。

tts.dispose();

所感

piper-plusのWASM版はnpmパッケージをインストールするだけでブラウザ上のTTSが実現できます。モデルのダウンロードはIndexedDBにキャッシュされるため、初回以降は高速に起動します。サーバーレスで完結する点は、プライバシーが重要なアプリケーションやオフライン対応に有用です。次回はOpenJTalkのアクセントラベルを使って日本語TTSの品質改善を試みます。