Seed-VCアーキテクチャ徹底解説 — 声を「誰が・何を・どう」に分解する4段構成
「録音した自分の声を、別人の声に変換する」——これを実現しようとすると、最初にぶつかる問いがあります。
そもそも “声” とは何でできているのか?
同じ台詞を喋っても、話者が違えば別の音になる。同じ人が喋っても、内容が違えば別の音になる。抑揚をつければまた変わる。つまり音声は少なくとも「誰の声か(話者性)」「何を喋っているか(発話内容)」「どう喋っているか(韻律)」という、独立した複数の情報が混ざり合った信号です。
声質変換(Voice Conversion)の難しさの本質はここにあります。話者性だけを差し替えたいのに、素朴に処理すると発話内容や抑揚まで巻き添えで変わってしまう。この記事では、私たちが「声のデザイン」アプリのバックエンドに採用した Seed-VC が、この問題をどう解いているか——4つのモジュールで情報を分離して扱う設計思想を、実際のロード処理と推論コードを追いながら解説します。
設計思想: 分離できるものは、別のモジュールに任せる
Seed-VC の中核にあるのは「一枚岩のモデルに全部やらせない」という発想です。声を構成する情報を種類ごとに専用のモジュールへ分解し、変換の最後にそれらを再統合します。実際、起動時のモデルロードはこうなっています。
model, semantic_fn, f0_fn, vocoder_fn, campplus_model, mel_fn, mel_fn_args = load_models(a)
一度に7つの部品が返ってきますが、役割で束ねると4段構成です。
- whisper (
semantic_fn) — 発話内容(何を喋っているか)を意味特徴として抽出する - campplus (
campplus_model) — 話者性(誰の声か)を1本のベクトルに埋め込む - CFM/DiT (
model.cfm) — 拡散モデルで、上記2つを条件にメルスペクトログラムを生成する - BigVGAN (
vocoder_fn) — メルスペクトログラムを実際に聴ける波形へ戻すボコーダ
「意味を取る担当」「話者性を取る担当」「絵(mel)を描く担当」「音にする担当」がきれいに分業している。この分業こそが、話者性だけを差し替えられる理由です。順に見ていきましょう。
第1段: whisper — 「何を喋っているか」だけを取り出す
発話内容の抽出には、音声認識モデル Whisper のエンコーダを使います。文字起こし(デコーダ)は使いません。エンコーダが出す中間表現(意味特徴の系列)を、そのまま特徴量として取り出します。
s_alt = _semantic(torchaudio.functional.resample(src_t, _S["sr"], 16000))
ポイントは、入力を 16kHz にリサンプルしてから通していること。Whisper は 16kHz 前提のモデルなので、変換モデル本体が 44.1kHz で動いていても、意味抽出の手前で必ず 16kHz に落とします。ここで得られる系列は「音の高さ」や「声色」をほとんど含まず、発話内容に対応した表現になっている——これが後段で話者性を自由に差し替えられる前提です。
なお Whisper は一度に30秒しか処理できないという制約があり、長尺音声ではチャンク分割が必要になります。これ自体が一本の記事になるほどの落とし穴でした(別記事「音声変換が『ゆっくり喋る』バグの犯人は、Whisperの30秒制限だった」を参照)。
第2段: campplus — 「誰の声か」を192次元に凝縮する
話者性の抽出は campplus(CAMPPlus)という話者埋め込みモデルが担当します。音声から fbank 特徴を作り、それを通すと 192次元のベクトル1本が返ってきます。
feat = torchaudio.compliance.kaldi.fbank(w16, num_mel_bins=80, dither=0, sample_frequency=16000)
feat = feat - feat.mean(dim=0, keepdim=True)
return _S["campplus_model"](feat.unsqueeze(0)).squeeze().detach().cpu().numpy()
この「話者性 = 1本のベクトル」という表現が、実は「声のデザイン」アプリの肝になっています。ベクトルなら、足したり混ぜたりできるからです。私たちは18人ぶんの話者のクリーン録音から作った埋め込みを「アンカー」として持ち、スライダー操作に応じてそれらを加重平均でブレンドしています。
w = design_weights(slider_values, bank, **kw)
emb = (w[:, None] * bank.embeddings).sum(axis=0)
「年齢感」「声の高さ」「ハスキー度」といった解釈可能な軸のスライダーから重み w を決め、アンカー埋め込みを重み付き平均する。話者性が独立した1本のベクトルとしてモジュール化されているからこそ、こういう「声を混ぜてデザインする」操作が成立します。話者性が発話内容と絡み合っていたら、混ぜた瞬間に内容も壊れてしまうでしょう。
第3段: CFM/DiT — 拡散で「メルの絵」を描く
意味特徴(内容)と話者埋め込み(話者性)が揃ったら、いよいよ再合成です。ここで働くのが拡散モデル(CFM: Conditional Flow Matching、estimator は DiT)。乱数ノイズから出発して、条件に合うメルスペクトログラムを段階的に生成します。
vt = _S["model"].cfm.inference(cat, torch.LongTensor([cat.size(1)]).to(dev),
mel2, style, None, STEPS, inference_cfg_rate=CFG)
vt = vt[:, :, mel2.size(-1):]
引数に注目すると、この段が各モジュールの出力をどう受け取っているかが見えます。
cat— 意味特徴(内容)。プロンプト音声ぶんpcと変換対象condを連結したものstyle— 第2段の話者埋め込み(=デザインしたブレンド埋め込み)mel2— プロンプト音声のメル。生成後にmel2.size(-1)ぶんを切り落として本体だけ残すSTEPS/inference_cfg_rate— 拡散のステップ数と classifier-free guidance の強さ
「内容 (cat)」と「話者性 (style)」を別々の引数で受け取っているのが分業の証拠です。モデルは「この内容を、この話者性で描け」という指示として両者を解釈します。片方を固定して片方だけ差し替えれば、内容はそのままに声だけ変わる。分離設計の狙いがそのまま推論のインターフェースに現れています。
韻律の扱い: length_regulator と F0
「どう喋っているか」——韻律(テンポ・音の高さの動き)は、少し性格が違います。内容と完全には切り離せないため、専用の1モジュールではなく、内容と話者性をつなぐ接続部で扱われます。
まずテンポ(長さ)。Whisper の意味系列は、生成したいメルのフレーム数へ引き伸ばす必要があります。これを担うのが length_regulator です。
cond, *_ = _S["model"].length_regulator(s_alt, ylens=torch.LongTensor([mel.size(2)]).to(dev),
n_quantizers=3, f0=None)
ylens に目標フレーム数を渡すことで、意味系列を狙った長さへ整えます。そして音の高さ(F0)。44.1kHz の F0 条件付きモデルでは、この length_regulator に f0 を渡せるようになっており、キャリアの声の高さを目標話者の音域に寄せる、といった韻律制御が可能になります。
lf[F0_alt > 1] = lf[F0_alt > 1] - m_alt + m_ori # キャリアF0を目標音域の中央値へ
shifted = torch.exp(lf)
cond, *_ = _S["model"].length_regulator(S_alt, ylens=tgt_len, n_quantizers=3, f0=shifted)
対数領域で中央値の差だけ平行移動する、というシンプルな処理です。韻律を「別モジュール」ではなく「条件として注入するパラメータ」として扱う——ここに、分離しきれない情報との現実的な折り合いのつけ方が表れています。
第4段: BigVGAN — メルを聴ける音に戻す
拡散モデルが描くのはあくまでメルスペクトログラム、いわば「音の設計図」です。人間が聴ける波形に戻す最後の一手がボコーダ、BigVGAN です。
wav = _S["vocoder_fn"](vt.float()).squeeze().detach().cpu().float().numpy().reshape(-1)
たった1行ですが、ここが音質を大きく左右します。同じメルでもボコーダが変われば聴こえ方が変わる。44.1kHz モデルが「本物の広帯域」を出せるのも、BigVGAN が高いサンプリングレートで学習されているからです(これも別記事「22.05kHz vs 44.1kHz」で詳述しています)。
落とし穴と学び
4段構成を実装して運用する中で得た教訓をいくつか。
- サンプリングレートは段ごとに違う。意味抽出は 16kHz 固定、変換モデル本体は 22.05kHz または 44.1kHz、話者埋め込みも 16kHz の fbank。「どの段が何 kHz を期待しているか」を取り違えると、無言で品質が劣化します。段ごとにリサンプルを明示的に挟むのが安全です。
- 話者性がベクトル1本だからこそ応用が効く。もし話者性が拡散モデルの内部に溶け込んでいたら、「声をブレンドする」機能は作れませんでした。モジュール分離は品質のためだけでなく、製品としての表現力に直結します。
- 韻律は完全分離できないものとして設計する。テンポと F0 は内容と絡むため、独立モジュールにせず「条件パラメータ」として注入するのが現実解でした。無理に切り離そうとすると、かえって不自然になります。
- 各段は差し替え可能。ボコーダは BigVGAN / HiFiGAN / Vocos が選べ、意味抽出は whisper / cnhubert が選べます。分業設計の副産物として、段ごとに最新モデルへ乗り換えられる拡張性が手に入ります。
まとめ
- Seed-VC の声質変換は whisper(意味) + campplus(話者性) + CFM/DiT(拡散mel生成) + BigVGAN(ボコーダ) の4段構成
- 設計思想は「声を “誰が・何を・どう” に分解し、種類ごとに専用モジュールへ任せる」こと
- 話者性を 192次元ベクトル1本として独立させたことで、複数話者のブレンド(声のデザイン)が可能になった
- 拡散モデルは内容 (
cat) と話者性 (style) を別々の引数で受け取る——分離設計がそのまま推論インターフェースに現れている - 韻律(テンポ・F0)は内容と絡むため独立モジュールにせず、
length_regulatorの条件パラメータとして注入する - 各段はサンプリングレートも役割も異なり、差し替え可能。分業は品質・表現力・拡張性のすべてに効く