Skip to content

アーキテクチャ一覧

HayaKoe は日本語テキストを受けて WAV 波形を生成するエンジンです。内部的にはひとつの「大きなモデル」が一度に処理するのではなく、複数段階のパイプライン に役割が分かれています。

このページでは入力テキストが音声波形として出力されるまで 各段階で何が起こるか を順を追って説明します。

用語に馴染みがなければ 用語集 をまず軽く目を通してから戻ってくることを推奨します。

全体パイプライン

入力テキスト(日本語)

  ├─ 1. 文分割(句読点基準)

  ├─ 各文ごとに:
  │    ├─ 2. G2P              — テキスト → 音素列 + アクセント
  │    ├─ 3. BERT             — 文単位の文脈埋め込み(話者共用)
  │    └─ 4. Synthesizer      — 音素 + BERT → 文単位の波形
  │         ├─ 4-1. Text Encoder
  │         ├─ 4-2. Duration Predictor
  │         ├─ 4-3. Flow
  │         └─ 4-4. Decoder

  ├─ 5. 文境界 pause       — Duration Predictor の再利用

  └─ 6. 波形連結 → 最終 WAV

この流れを以下で段階ごとに辿っていきます。

段階ごとに辿る

1. 文分割

入力テキストはまず 文単位で分割されます。 日本語句読点()と ASCII 句読点(!?)および改行(\n)を基準に分けられ、以降のすべての処理は文ごとに 個別に 行われます。

HayaKoe が文を一括で入れず分割する理由は 品質のため です。長い文をまるごと合成すると抑揚が崩れたり(prosody 不安定)、BERT が捉える文脈が過度に長くなって特定の音節が誇張される傾向があります。文単位で区切ると各文ごとに安定した韻律が保証されます。

トレードオフが2つあります。

第一に、元のモデルが通文合成で自然に生成していた 文間の休止(pause) が分割によって失われます。この問題は後方の 5. 文境界 pause 段階で Duration Predictor を再利用して解決します。

第二に、GPU 経路では分割が速度面で不利に作用 します。GPU は文1つを合成するのにかかる時間がそもそも短いです。分割するとこの短い合成を文の数だけ繰り返す必要があり、文間の pause 長を計算するプロセスも追加されます。結果として通文合成する方が総時間が短くなります。

HayaKoe は品質の利点がこのコストを正当化すると判断し、分割をデフォルト動作として維持しています。CPU 経路では逆に長い文を丸ごと合成するコストの方がはるかに大きいため、分割が 速度面でも有利 に作用します。

2. G2P — テキスト → 音素列 + アクセント

各文を受けた後にまずやるべきことは 文字を発音に変えること です。HayaKoe は日本語 G2P を pyopenjtalk に委譲します。

G2P が処理するのはひとつやふたつではありません。漢字の読み(天気てんき)、長音規則、連音 (sandhi)、アクセントタイプ(pitch accent)の決定など日本語特有の規則がすべてここで解決されます。出力は単純な音素の羅列ではなく 音素シーケンス + アクセント情報 のペアです。

この段階は完全に CPU・Python 上で動作し、モデル推論とは無関係です。pyopenjtalk の内部辞書ファイルが wheel 内にバンドルされているためネットワーク接続なしでも動作します(→ OpenJTalk 辞書バンドル)。

3. BERT — 文脈埋め込み

G2P とは別に、原文テキスト自体 を DeBERTa BERT に入力して文脈埋め込みを取得します。各トークンごとに「この文でこの単語がどんな役割か」を含むベクトルが出力されます。

BERT 特徴は以降の Synthesizer の合成品質に直接反映されます。同じ音素列でも BERT が見た文脈によって抑揚・強勢が変わります。

例えば そうですね という同じ応答でも、前に強い主張が来れば確信に満ちた相槌トーンで、前にためらいが来れば余韻の残る共感トーンで発話されるのが自然です。G2P は音素列しか見ないためこの2つのケースを区別できず、BERT が周辺文脈情報を圧縮して Synthesizer に渡すことでこのような差異が合成に反映されます。

これが Bert-VITS2 系列が元祖 VITS 比で自然な発話を生み出す核心メカニズムです。

重要な構造的特徴は BERT が全話者共用 であるという点です。BERT は全モデルパラメータの約 84%(~329 M、FP32 基準で約 1.2 GB)を占める 最もサイズの大きいモジュール です。一方、話者ごとに異なる Synthesizer は 63 M(約 251 MB)規模です。

この非対称のおかげで BERT を一度だけロードし全話者が共有する構造が決定的です。話者 N 名を同時にサービングしても BERT は1回だけロードされ、各話者は 251 MB ずつ追加されるだけです。多話者サービングでもメモリ使用量が線形に爆増しない理由です。

追加で、多文を処理する際は複数文を ひとつのバッチにまとめて BERT を1回だけ呼び出し ます。GPU ではカーネルランチオーバーヘッドが減って文数に比例して利得が大きくなり、CPU では利得・損失がほぼないためバックエンド間のコード統一のために同一経路を維持しています(→ BERT GPU 保持 & バッチ推論)。

4. Synthesizer — 音素 + BERT → 波形

ここが実際に音声が作られる部分です。入力は (音素列, アクセント, BERT 埋め込み, スタイルベクトル)、出力は その文分の波形 です。

この中の スタイルベクトル.load(speaker) 時点で該当話者の style_vectors.npy から一緒にロードされる値で、話者の話し方・トーン特性をひとつのベクトルに圧縮した表象です。HayaKoe は現在簡素化のため Neutral スタイルのみを使用しており(→ 用語集 — Style Vector)、毎回の合成呼び出しでこの値がそのまま注入されます。

Synthesizer 内部は4つのサブモジュールに分かれており、テキスト情報がこの順序で流れながら波形に変換されます。

4-1. Text Encoder — 音素をベクトル空間へ

Transformer エンコーダー構造で、各音素を 192次元隠れベクトル に埋め込みます。BERT 特徴がここで音素レベルの埋め込みと結合され、文脈が初めて音素レベルの情報に注入されます。

出力の shape は (音素数, 192) で、以降の2段階の入力として共有されます。

4-2. Duration Predictor — 各音素を何フレーム発音するか

Stochastic Duration Predictor (SDP) が各音素に対して「何フレーム発声するか」を確率分布からサンプリングして決定します。「あ」は5フレーム、「い」は4フレームという具合です。確率サンプリングのため同じ文でも呼び出しごとに抑揚・速度が少しずつ変わります。

この段階で音素シーケンスは 時間軸方向に伸長されます。 各音素が自分の持続長分だけ繰り返しコピーされてつなぎ合わされると理解すれば良いです。結果の長さは最終オーディオのフレーム数に直結します。

4-3. Flow — テキスト埋め込み → 音声 latent

Normalizing Flow の逆方向変換を経て、Text Encoder が生成した埋め込みを 音声 latent 空間の z ベクトル へ移します。この z は「どんな音であるべきか」を含む低次元表象で、次の Decoder がこれを受けて実際の波形を生成します。

Flow は可逆神経回路網のため学習時には反対方向(正解音声 latent → テキスト埋め込み空間)で使われ、推論時にはその逆方向を辿ります。

4-4. Decoder — latent z → 波形

HiFi-GAN 系列の Decoder で、latent z を受けて 時間ドメインの実際の波形 を生成します。ConvTranspose アップサンプリングと残差ブロック(ResBlock)で構成されています。

Synthesizer サブモジュール中で計算量が最大で、推論時間の大部分がここで消費されます。CPU 経路で ONNX にエクスポートする際もこの部分が最適化の主対象です。

この4段階をすべて経ると、その文ひとつに対する 44.1 kHz 波形が完成します。

5. 文境界 pause 予測

1段階 で言及した「文間の休止消失」問題を解決する段階です。

HayaKoe は Duration Predictor を本来の用途以外にもう一度再利用 します。全体の原文テキストを Text Encoder + Duration Predictor までのみ通過させて .!? 句読点位置のフレーム数を得て、これを秒単位の pause 時間に換算します。Flow と Decoder はスキップするため追加コストが低いです。

結果的に固定 80 ms の無音ではなく 文の長さ・構造に応じて自然に変動する pause が生成されます。詳細は 文境界 pause — Duration Predictor で扱います。

6. 波形連結 → 最終出力

最後に各文の波形を順番につなぎ合わせ、文と文の間に 5段階 で予測した長さ分の無音サンプルを挿入します。

最終出力は 44.1 kHz・単一チャネル float32 波形(NumPy array)です。.save() を呼ぶと WAV ファイルとして保存されます。

Streaming API(astream())を使用すると文がひとつ完成するたびに即座に yield されるため、全体合成が終わる前に再生を開始できます。長いテキストで 最初の音声までの遅延を大きく縮める 効果があります(→ ストリーミング例)。

次に読むもの