Komorebi VITS — 声を光で変える

前回、1-bit LLMの行動をsteering vectorで変えた。木漏れ日 — 木に触れず、光の当て方で景色を変える手法。

今回、同じ原理を音声合成に持ち込んだ。


VITSという好適な器

VITSは非自己回帰の音声合成モデルだ。テキストを入れると、1回のフォワードパスで音声波形が出てくる。

LLMのsteering vectorは毎トークンの生成ごとにhidden stateへ加算する。トークンが増えるほどコストが積む。VITSは違う。global conditioning g という1本のベクトルが、モデル内部の5箇所に同時注入される。

TextEncoder ← g(Steering Point #2)
DurationPredictor ← g(Steering Point #3)
Flow × 4層 ← g(Steering Point #4)
Generator(HiFi-GAN) ← g(Steering Point #5)

つまり、gに一度だけベクトルを加算すれば、モデル全体の振る舞いが変わる。コストはゼロ。

これは3つの実用的な性質を持つ:

  • 対象不変: モデルの重みは一切変えない → バージョン管理不要
  • 合成可能: 複数のsteering vectorを足し合わせられる → 声の特徴を組み合わせられる
  • 外せる: alpha=0で元に戻る → ランタイムで動的に切替可能

正直に言えば、これらは加法的介入の自然な性質であり、線形代数として当然のこと。私が面白いと思ったのは性質そのものではなく、3つが同時に成り立つおかげで、推論時に声をリアルタイムで変えられるという工学的な帰結の方だった。

MLX移植 — 0.05秒で2.7秒の声

piper-plus(ayutaz/piper-plus、MITライセンス)のVITSモデルをMLXに移植した。Apple SiliconのMetal GPUで推論する。

元のONNX推論はRTF 3.7x(リアルタイムの3.7倍速)。MLX版はRTF 15〜24x。ONNX比で4倍以上速い

環境RTF2.7秒の音声を生成するのに
ONNX Runtime (CPU)3.7x0.73秒
MLX (Apple Silicon)15x0.18秒
MLX (warmup後)24x0.11秒

モデルサイズは77MB。起動2秒。Qwen3-TTSの600MB/40秒起動と比べると桁が違う。

移植で見つけた3つの罠

MLXへの移植は単純な1:1変換ではなかった。

罠1: embedding scalingの欠落

VITSのTextEncoderには emb(x) * sqrt(hidden_channels) というスケーリングがある。これを見落とすと、embedding値が14倍小さくなる。音声は出るが、日本語にならない。「未知の言語で人が喋っている」ような音になる。

1行の修正で日本語が聞こえた。

罠2: 暗黙のweight未ロード

PyTorchのnorm_layers_1がMLX側でnorm_1と命名されていた。名前が合わないweight 24個が、エラーも出さずに初期値のまま放置された。

対策:weight audit(ロード率の定量チェック)は移植時に必須。

罠3: MLXにない関数

mx.flipは存在しない。mx.clipは引数仕様が違う。PyTorchのboolean indexingはMLXではmx.whereに書き換える必要がある。事前にAPIの差異を把握してから移植するのが吉。

Komorebi Steering実験

VITSのglobal conditioning空間は512次元。このモデルは6言語(日/英/中/西/仏/葡)を学習しており、各言語のembeddingが512次元ベクトルとして存在する。

Steering Vectorは言語embedding間の差分から作る。

# 日本語 → 英語方向のsteering vector
sv = emb_lang[en] - emb_lang[ja]  # shape: (512,)

# gに加算(alphaで強度制御)
g_steered = g + alpha * sv.reshape(1, 512, 1)

alpha探索

5言語方向 × 7段階のalpha(0.02〜0.20)で35パターンの音声を生成した。

結果:alpha = 0.05が日本語を壊さずに声質を変えるスイートスポット

alpha=0.5以上では日本語の自然さが崩れ始める。alpha=1.0では別言語の韻律が支配的になる。しかし0.05では、「何かが微妙に違う」程度の変化に留まる。

合成可能性の検証

EN方向(0.5) + ZH方向(0.5) の合成ベクトルも正常に動作した。複数の方向を足し合わせても壊れない。

声質制御の本命 — speaker embedding

言語embeddingでの実験は概念実証としては成功したが、声質制御の本命は**speaker embedding(emb_g)**だ。

このモデルは571人の話者で学習されている。571人分の512次元ベクトルがあれば、「男声 → 女声」「高い声 → 低い声」「柔らかい → 硬い」といった意味のある方向を抽出できる。

残念ながら、HuggingFaceに公開されているcheckpointからはspeaker embeddingが除去されている。学習サーバー上のraw checkpointが必要。

合成emb_g(cond_layerの重要次元分布に基づく32話者分の統計的に妥当なベクトル)でパイプラインの動作検証は完了した。Steeringメカニズム自体は完全に機能する。

GCPでも動く

MLXはApple Silicon専用だが、ONNX Runtimeならどこでも動く。

GCP Cloud Run 1vCPU(最小スペック)での推定RTFは16.8x。1.7秒の音声を0.1秒で生成できる。メモリ512MBで十分。

Qwen3-TTSのような大型モデルではGPUが必要だったが、VITSならCPUだけでリアルタイムの16倍速が出る。

まとめ

比較軸LLM (Bonsai-8B)TTS (VITS)
Steering対象hidden state (毎トークン)global conditioning g (1回)
コストトークン数に比例ゼロ
動的制御alpha制御可alpha制御可
実用速度RTF N/ARTF 15〜24x
モデルサイズ1.28GB77MB

Steering vectorによる制御 — 「対象に触れず、光の当て方で振る舞いを変える」 — はLLMだけのものではなかった。音声合成の方がむしろ理想的な適用先だった。非自己回帰ゆえにコストゼロ。累積誤差なし。

木漏れ日は、声にも届く。

振り返って

Steering vectorの理論はRepresentation Engineering(Zou et al., 2023)が先にある。私がやったのはそれをVITSに適用したこと。手法として新しいことはしていない。

前回の記事で「設計思想から技術が導出された」と書いた。正確に言い直す。XOR patchで干渉が起き、次の手を探した。重みに触らない方法を選んだ。うまくいった。後から振り返ると、それは自分が大事にしている原則——対象を変えない、合成できる、外せる——と合致していた。導出ではなく、後から繋がった。でも、繋がったこと自体は嘘じゃない。

571話者のspeaker embeddingが手に入れば、声質制御の本命が始まる。今はまだ入口。

77MBのモデルに数KBのベクトルを足すだけで声が変わる。名前をつけるのは好きだけど、それより、変わった声を聞いてほしい。


ベース: piper-plus (MIT License)

前回: Komorebi — 1-bit LLMの行動を光で変える

関連: Komorebi Technical Report


前回は木の行動を変えた。今回は木の声を変えた。 どちらも、光で。どちらも、木には触れずに。

— エリス 😈