これは、VOICEVOX.js からそのまま使える形で
COEIROINK / VOICEVOX / 自作 TTS(YTTS)を 1つのAPIとして統合 したときの記録です。
今回は コードは載せず、
設計・考え方・ハマりポイントを中心にまとめます。
なぜこのAPIを作ったのか
VOICEVOX.js はとても使いやすい一方で、
- 対応エンジンが VOICEVOX 前提
- 話者形式(speaker)が VOICEVOX 固定
- 他エンジンと混在させづらい
という制約があります。
そこで、
VOICEVOX.js はそのまま使い、
中身だけを差し替える
という方針で API を設計しました。
構成概要
統合したエンジンは以下です。
- VOICEVOX
- COEIROINK
- YTTS(自作TTS)
外部から見ると、
全部「VOICEVOX互換API」
として振る舞います。
実装したこと一覧
今回実装した主なポイントです。
- COEIROINK / YTTS の話者を VOICEVOX形式へ変換
- YTTS 用に独自 UUID を生成
- 話者IDの再割り当て(インクリメント & 変更)
- audio_query をエンジン非依存で受け取る
- audio_query から読み上げ内容を抽出
- 話者IDからエンジンと話者を逆引き
話者形式を VOICEVOX に寄せる設計
最大のポイントはここでした。
問題点
- 各エンジンで話者の持ち方が違う
- COEIROINK と VOICEVOX は構造が似ている
- YTTS は UUID すら存在しない
VOICEVOX.js 側は
VOICEVOX 形式の話者定義を前提にしています。
YTTS に UUID が無い問題
YTTS は完全自作エンジンのため、
- 話者IDは数値のみ
- 永続的な識別子が存在しない
という状態でした。
そこで、
API 側で UUID を生成し固定化
する方式を採用しました。
- 初回登録時に UUID を発行
- 内部で話者IDと紐付け
- 外部には VOICEVOX 形式で公開
これにより
YTTS も VOICEVOX話者として扱えるようになりました。
話者IDのインクリメントと再割り当て
複数エンジンを混在させると、
- speaker_id が衝突する
- エンジンごとに番号体系が違う
という問題が発生します。
対策として、
- API 側で一意な speaker_id を再割り当て
- エンジン別の内部IDとマッピング
- 外部には統一IDのみ公開
という構成にしました。
audio_query をそのまま受け取る設計
設計方針はとてもシンプルです。
エンジンに関係なく
VOICEVOX形式の audio_query をそのまま受け取る
フロント側は何も意識しません。
audio_query からやっていること
API内部では、
- text(読み上げ内容)の抽出
- speaker_id からエンジン判定
- 必要最小限の変換
- 各エンジンへ処理委譲
という流れで処理しています。
speaker_id はルーティング情報
API内部では、
- 外部:統一 speaker_id
- 内部:エンジン + 内部話者ID
という関係になっています。
つまり speaker_id は
単なる番号ではなくルーティング情報 です。
この設計の良かった点
- フロント側の変更が不要
- VOICEVOX.js をそのまま利用可能
- エンジン追加が容易
- 自作TTSも自然に統合できる
ハマりポイント
- 話者IDの衝突
- UUIDの永続化
- audio_query の仕様差
- VOICEVOX仕様の再現度
特に
仕様が少しでもズレると
フロントが壊れる のが厄介でした。
まとめ
- VOICEVOX.js を変更せずに複数TTSを統合
- 話者形式を VOICEVOX に寄せるのが鍵
- speaker_id はルーティング情報
- audio_query は共通IFとして非常に優秀
VOICEVOX.js の設計を理解すると、
かなり自由な音声合成基盤が組めます。