7月31日、Zack Proserが「The Voice Box: Running Your Own ElevenLabs, Offline and Free」と題した記事を公開した。Fish Audio S2 ProとAppleのMLXフレームワークを組み合わせ、有料APIを使わずにローカルで動作する音声合成スタジオを構築する方法について、落とし穴や実測値も含めた詳細な記録となっている。
なぜローカルTTSなのか
有料の音声合成APIには3つの問題がある、とZack Proserは指摘する。
1つ目はプライバシーだ。NDA下の作業や未公開の原稿が、毎回APIコールのたびに外部サーバーへ送信される。1時間に20回イテレーションすれば、20回のテキストが第三者に渡る。
2つ目はコンテンツポリシーの不透明さだ。ベンダーは火曜日と木曜日の間にエンドポイントのポリシーを変更できる。同じポリシーが、フィッシング啓発トレーニングを書くセキュリティ研究者と悪意ある攻撃者の両方に適用される。
3つ目はレイテンシの予測不能性だ。ネットワーク遅延が乗るリモートモデルよりも、2秒かかるローカルモデルの方が体験として優れていることがある。飛行機の中でも動く。
コストの観点では、ローカル実行の課金対象は電気代のみとなるため、文字数を気にせず何度でも音声を生成できる。
Fish Audio S2 Proとは
今回使用したモデルはFish Audio S2 Pro(パラメータ数50億)だ。2026年3月に技術レポートと合わせて公開された。
アーキテクチャは2段構成になっている。
- 4Bの「slowモデル」:何を、どのように話すかという意味的な判断を担う
- 400Mの「fastモデル」:その決定を音響的な詳細に展開する
- 最後にニューラルコーデックが波形を生成する
重い処理がセマンティックステップの頻度でのみ実行されるため、50億パラメータのモデルがラップトップ上でリアルタイムに動作できる。対応言語は80以上、学習には1000万時間超の音声データが使われた。
ライセンスはFish Audio Research Licenseで、元記事によれば研究・非商用利用は無料で利用可能とされている。商用利用については元記事でも詳細条件は明示されていないため、Fish Audioの公式ページで最新の条件を確認することを推奨する。
Apple Silicon(MLX)での実行
公式のリファレンス実装はSGLangを使いNVIDIA GPUを前提としており、Apple Siliconでは1文の生成に25〜70秒かかるとの報告がある。これでは使い物にならない。
そこでAppleの**MLXフレームワークと、S2 Proのネイティブバックエンドを持つmlx-audio**を使用した。セットアップは仮想環境の作成とパッケージのインストールのみだ。
uv venv --python 3.12
uv pip install mlx-audio
M5 Max(128GB統合メモリ)での実測値は以下の通りだ。なお、RTF(Real-time Factor)とは「処理時間 ÷ 生成される音声の長さ」を表す指標で、RTFが1.0を超えると音声が再生時間より速く生成されることを意味する。
| 生成パターン | 音声長 | 処理時間 | RTF | ピークメモリ |
|---|---|---|---|---|
| インラインタグあり(初回) | 12.07s | 14.7s | 0.82× | 17.2GB |
| 2話者ダイアログ | 7.57s | 7.5s | 1.02× | 17.9GB |
| 短い1文 | 3.76s | 3.3s | 1.15× | 15.9GB |
| 完成アプリ経由 | 10.36s | 9.78s | 1.06× | — |
概ねRTF 1.0前後を維持しており、10秒の音声が約10秒で生成される。重要な点として、2回目以降はモデルがキャッシュから読み込まれるため起動が1.1秒で完了し、インタラクティブな編集に十分なレスポンスを得られる。
約17GBのピークメモリはモデル本体+コーデック+アクティベーションの合計だ。128GBマシンなら、70Bのテキストモデルを同時に常駐させても余裕がある。
最大の落とし穴:ボイスライブラリをゼロから作る必要がある
S2 Proは純粋なゼロショットクローナーであり、組み込みのスピーカーIDや名前付きボイスは一切持たない。これがこのプロジェクトの工数の半分を占めた。
CLIには--voiceフラグが存在するが、実は内部で即座に破棄されている。
del voice, repetition_penalty, verbose, kwargs
参照クリップなしに生成すると、モデルは毎回異なる話者を生成する。同じテキストで2回生成した際の基本周波数の中央値は205.1Hzと167.7Hzで、明らかに別人の声になった。
解決策は参照クリップのライブラリを自前で構築することだ。参照クリップを渡すと、4つのプリセットで測定した平均周波数ばらつきが21.2Hzから5.1Hzへ、約4分の1に収まった。
プリセット名はクリップ自体のピッチではなく、そのクリップを参照した際に実際に出力される音声の周波数から命名する。参照クリップの周波数は実際の出力より10〜40Hz高く出るため、ラベルが予測値とズレてしまうからだ。
また、ボイスのクローニングには音声と完全一致するテキストのトランスクリプトが必須であり、これが最も多いクローニング失敗の原因だと記事は強調している。
2つのバグと修正
このセクションはWebアプリとしての実装を行う開発者向けの内容だ。
Webアプリへの移行時に、ランタイムの未文書動作による2つのバグが発生した。
バグ1:参照音声はデコード済み配列で渡す必要がある
READMEにはファイル名を渡す例が載っているが、実際にはモデル内部で'str' object has no attribute 'ndim'エラーが発生する。CLIと同様に、事前にデコードした配列を渡す必要がある。
from mlx_audio.utils import load_audio
kwargs["ref_audio"] = load_audio(str(ref_path), sample_rate=model.sample_rate)
バグ2:MLXのMetalストリームはモデルをロードしたスレッドに束縛される
メインスレッドでモデルをロードし、HTTPハンドラースレッドで生成を実行するとRuntimeError: There is no Stream(gpu, 0) in current threadが発生する。解決策は専用のエンジンスレッドを1つ立て、モデルのロードと生成の両方をそのスレッドに集約することだ。HTTPハンドラーはキューに生成リクエストを投入して結果を待つだけに留める。
完成したアプリの機能
Webアプリはテキストボックスとボイスのドロップダウンを中心に構成されている。約30種類のインラインタグで単語レベルの発話制御が可能だ。
Well, this is convenient. [pause] I can say anything now, [chuckle] and it never leaves this laptop.
使えるタグには[pause]、[whisper]、[laughing]、[sigh]、[shouting]、[excited]、[with strong accent]などが含まれる。
また、スピーカータグを使えば1回の生成で2話者のダイアログも描写できる。
なお、生成音声への透かし(ウォーターマーク)は埋め込まれない。mlx-audioパッケージにはウォーターマークエンコーダーのブロックが存在するが、Fish S2 Proのパスはそのモジュールを呼び出さない。これは技術的な事実として理解しておくべき点だが、生成音声の出所を証明する手段が存在しないことを意味する。ディープフェイク音声やなりすましへの悪用リスクが社会的に議論されている中、ローカルTTSを利用する際には利用者自身が倫理的・法的な責任を意識する必要がある点は付記しておきたい。※編集部の考察
詳細はThe Voice Box: Running Your Own ElevenLabs, Offline and Freeを参照していただきたい。