注:筆者は韓国在住のため、本文には韓国特有の文脈が含まれることがあります。

2年ほど前にBert-VITS2という日本語TTSライブラリを知り、学習方法を一度まとめた記事を書いたことがあります。

その後は、学習させたモデルを自宅クラスターサーバーに乗せて普段使いしていました。元のコードをForkした上で、音声合成に必要なpathだけを切り出して整理して使っていました。

ところが時間が経つにつれ、小さな不便がじわじわと積み重なり始めました。

  • 日本語テキストを発音情報に変換してくれるopenjtalkライブラリが、外部から何かを継続的にダウンロードしていて、それがどう動いているのか実はよく分からない。 とりあえず使ってはいたものの、もしあのダウンロードサーバーが落ちたら自分のサーバーも一緒に死にそうで、なんとも気持ち悪かったです。
  • openjtalkのせいでPythonのバージョンアップが止まっていた。 依存関係の互換性問題で、しばらくの間アップグレードできずに放置していました。
  • TTSが英語に対応していない(無音になる)。 前段に翻訳レイヤーを挟んで一度日本語に置き換えてから合成する形で回避していました。動きはしましたが、地味に気になっていました。
  • そして決定打として、既存ライブラリの多くがfish-speechという新しいライブラリに移行しており、それを継承したStyle-Bert-VITS2 (SBV2) も8ヶ月前にアップデートが止まっていたことです。

どうせ維持するだけなら、この機会に全部作り直して自分が使いやすい形にしてみよう。

というのがこのプロジェクトの始まりでした。そしてどうせ手をつけるなら、モデルコードまで深く覗き込んで最適化の余地があるか勉強してみよう、というのが二つ目の目的でした。

成果物 — hayakoe

おおよそこのように使うライブラリになりました。

python
from hayakoe import TTS

tts = TTS().load("jvnv-F1-jp").prepare()
tts.speakers["jvnv-F1-jp"].generate("こんにちは").save("output.wav")

特徴をまとめると:

  • openjtalkの外部ダウンロード排除 — 辞書を同梱しているので、初回実行時も外部サーバー依存なし
  • 最新Python対応 — Python 3.x新バージョン環境ですぐにインストール/動作
  • 英語入力の直接合成 / カスタム辞書登録 — 22万エントリの外来語辞書内蔵で英語→カタカナを自動変換、未知の固有名詞は直接発音登録可能
  • CPUリアルタイム推論 — PyTorch比1.5×〜3.6×速く、RAMは半分程度(5,122 MB → 2,346 MB、-54%)に減少
  • torch不要 — CPU推論ではPyTorchなしで動作、依存関係が軽量
  • 簡潔なAPITTS().load(...).prepare()一行で開始 / HuggingFace transformersスタイル
  • ソースのプラガブル化hf://, s3://, file://を一つのインスタンスで混在利用可能
  • FastAPIサーバー統合対応 — 同期/非同期ハンドラ両方をサポート
  • ARM64対応 — Raspberry Pi 4B上でも動作、ただし速度は遅い

「で、結局何をしたら速くなったの?」

最初は量子化でも一回試してみようかな、くらいの軽い気持ちで始めました。

weightファイルのサイズだけ見ても、BERTがモデル内で占める割合が一番大きかったんです。なので「これさえ量子化すれば良いだろう」と思っていたのですが、いざ計測してみるとそうではありませんでした。

  • テキストが長くなるほどSynthesizerがCPU時間の80〜91%を占め、BERT量子化で削れる時間は全体の5%にも満たなかったです。
  • BERT INT8量子化は速度ではなく、**メモリ削減(1,698 MB → 368 MB、-78%)**にこそ実際の価値がありました。
  • 「ならSynthesizerを量子化すれば良いんじゃないか」と思ったのですが、Synthesizerは量子化自体が不可能でした — Flowレイヤー内のrational_quadratic_splineがFP16から数値不安定で壊れるからです。

つまり、どこが実際のボトルネックか正確に計測しないと、見当違いのところに時間を注ぎ込むことになる、ということを毎回また学び直した形です。

そんなわけで、結局作業はおおよそこんな流れになりました。

  1. モデルから選び直し — 2年という時間が経ったので、どうせ作り直すなら他のライブラリも一度見回してみようと思いました。韓国語対応が良いというGPT-SoVITS、autoregressive系(Qwen-3-TTSなど)を候補に検討しました。リアルタイムアラーム/ブリーフィング用途なので遅延時間がそれなりに重要なファクターで、結局音質・速度のバランスが一番良かったStyle-Bert-VITS2 (JP-Extra v2.7.0)に落ち着きました。

    • GPT-SoVITS — リアルタイム速度は満足だったものの、推論音声の体感品質がSBV2より劣り脱落。
    • Qwen-TTS — 演算量が多すぎて脱落。既存GPUがRTX 2070 Superだったのですが、flash-attnがRTX 3000番台以上のカードでしかサポートされていないと知り、「これflash-attn入れてないせいかも?」と思ってRTX 3080まで新しく買ってみたのですが、生成時間が長くてリアルタイム使用は無理という結論に至りました。
  2. コンポーネントごとに異なる処方を適用 — 計測結果に基づき、BERTとSynthesizerにそれぞれ異なる最適化を適用しました。

    • BERT — INT8 dynamic quantizationでメモリだけ削減。速度には大きく効きませんが、一台のサーバーで複数プログラム動かそうとするとメモリがかなり重要なので。
    • Synthesizer — ONNXに変換してグラフレベルの最適化を適用しました。ONNX Runtimeがkernel fusion(複数演算を一つに統合)、constant folding(定数演算をロード時点で事前計算)のような最適化を自動で適用してくれます。
  3. 細かいディテールの仕上げ — 大筋以外の細部も合わせて整えました。

    • torch.compile — GPU推論でPyTorch JITコンパイルによる速度の追加向上。
    • BERTバッチ推論 — 複数文合成時にBERT呼び出しを一回にまとめてオーバーヘッドを削減。
    • 自然なpauseの復元 — オリジナルのSBV2は句読点の後に自然な間が入るのですが、複数文を分割合成するとその間が消えてしまいます。Duration Predictorだけ別途ONNXとしてexportし、句読点位置の間の長さを予測するように処理しました。
    • ARM64ビルド — Raspberry Pi 4BのようなARM機器でも動作するよう事前ビルド。
  4. ライブラリとしての設計 — 単に動くだけで終わらせず、他の人も簡単に持っていって使えるようにする部分に気を配りました。

    • 話者ごとのメモリ分離 — BERT、WavLMのような共有リソースは一度だけロードし、話者ごとに自分の重みだけを別途保持するように構造化。話者が増えてもメモリが爆発しません。
    • multi-sourceサポート — 一つのインスタンスでhf://, s3://, file://を混ぜて使用可能。公式モデルはHuggingFace、すでにS3を使っているならS3、開発中のモデルはローカル、といった形で。
    • Thread-safeなシングルトンサービング — FastAPIサーバーで一つのTTSインスタンスを複数ハンドラが共有しても安全。
    • Dockerビルドパターン — ビルド時点でモデルをイメージに含め、ランタイムではキャッシュから即座にロード。

これから書く記事

このシリーズは4部作で書く予定です。

  1. (Part 1) 2年ぶりにTTSモデルをあれこれ見て、また再びVITSに落ち着いた話 GPT-SoVITS v4を試した上でなぜ乗り換えたのか、学習データセットをどう集めたのか、10チェックポイントの聴感評価で最適stepをどう決めたのか。
  2. (Part 2) メモリは半分に、速度は1.5倍速くした話 ボトルネック計測の方法論、量子化実験(なぜSynthesizerは量子化できないのか)、ONNX変換、そしてメモリが89MBと誤計測されていた事件。
  3. (Part 3) 最後の1%まで — torch.compile、バッチ推論、Pause復元、ARM64 GPU推論の仕上げ作業と、Duration Predictorだけ別途切り出してONNXにexportした話。
  4. (Part 4) ライブラリとしてパッケージしながら考えたこと API設計哲学、multi-source(hf:// / s3:// / file://)、thread-safeなシングルトンサービング、FastAPI / Docker配布。

序論を終えるにあたって

最初に学習記事を書いたときは「これ一回学習して終わり」くらいに軽く考えていたのですが、いざモデルを手にしてみると、学習そのものよりもこれをどうやって他の人にも簡単に持っていって使ってもらえるようにするかの方がずっと大きな仕事でした。

その過程で普段触らない量子化・ONNXグラフ・MLサービング領域まで足を踏み入れることになったのですが、せっかくやったついでにまとめておけば、似たような道を歩む方にも役に立ちそうだと思い、シリーズで残すことにしました。