Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ストリーミング音声合成 #1492

Open
Yosshi999 opened this issue Nov 27, 2024 · 8 comments
Open

ストリーミング音声合成 #1492

Yosshi999 opened this issue Nov 27, 2024 · 8 comments

Comments

@Yosshi999
Copy link
Contributor

内容

VOICEVOX/voicevox_core#866 をEngineに対応させ、ストリーミングで音声が返ってくるAPIエンドポイントを作成する

Pros 良くなる点

レイテンシの短縮

Cons 悪くなる点

なし

実現方法

  • FastAPIにStreamingResponse があり、バイナリを吐くイテレータを渡すことでストリーミング化できる
  • チャンク幅をどうすべきか?最適なチャンク幅はエンジンが動作するマシンの火力に依存するが、その最適幅をどう計算するべきか(もしくはコンフィグで指定?)
  • 音声形式をどうすべきか?
    • PCM 16bit LE (application/octet-stream) : Coreが吐くf32波形をいじるだけなのでシンプルだが、sampling rateなどのメタデータを別途渡す(or クエリとして受けとって反映する)必要あり
    • Opus (audio/opus) : 正直よくわかっていないがストリーミング向けらしい。各種ブラウザが対応しており、Discordの場合はストリームを流すだけでそのまま再生できるらしい
    • WAV (audio/wav) : 普通の/synthesisと同じようにWAVで返す。

いずれのケースでもチャンク化されたバイナリをどうやって再生するか・扱いやすいか考える必要がありそう

その他

@Hiroshiba
Copy link
Member

Hiroshiba commented Nov 27, 2024

Stream 用のAPI の仕様を考えないとですね!!
ちょっとエディタ周りで興味ありそうな方や、仕様を調査するのとかが強そうな方にメンションします、もしよかったらコメントいただければ!! 🙏
@y-chan @romot-co @sigprogramming @sabonerune @takana-v @sevenc-nanashi

今回はトークのストリーミング用音声を作るAPIの仕様ですが、よほどのことがなければソングも似たような API にするつもりです。
なのでそのままソングでも適用できそうな形になっていると良さそう!

以下考えた点です:

  • どういうレスポンスにするか
    • 「1つのwavファイルを徐々に返す」が良さそう(コメント求む)
    • Opus形式は一旦考えない。VOICEVOXは大半がローカル通信なので圧縮がいらない。後々実装を考えてもいいかも。
    • RAWを返すかWAVを返すかだけど、一塊を細切れに返すなら多分どっちでもエディタ側の手間はそんなに変わらない
      • ヘッダーが付かないならヘッダーをつけるだけ
      • ヘッダーが付くならヘッダーを除くだけ
      • ビットレートとかを取得する方法を考える手間を省けるのでWAVのが楽かも
    • エンジンを使うユーザーにとってはWAVが降ってくるのが一番楽そう、そのまま再生すれば良いので
      • 多分トークエディタにとってもこれが一番良い
    • サンプルレベルでストリーム処理をする場合、処理を書くのが耐えられるレベルなのかちょっとわからない
      • 最初ヘッダーを返し、あとはチャンク幅ごとに返す、みたいな形には作れる
      • でも「Nサンプル取得する」みたいな形には書けない
      • 直感バッファリングすればよさそうだけど、ちょっと自信がない・・・。
    • 「指定した範囲の音声を返す」APIも考えられるけど、サーバー通信の面の実用性を考えるとWAV返すほうが良い気がする
      • もしどうしても必要になった場合はこのタイプの API も足せば良さそう。
  • ストリーミング用の引数はAudioQueryに追加するか、APIの引数に追加するか
    • AudioQueryではなく、ストリーミング用APIの引数に追加する
    • AudioQueryは「音声を表現するためのクエリ」で、ストリーミング用の引数はストリーミングを制御するためなので
  • チャンクサイズの指定方法
    • トークは秒、ソングはフレーム数で指定
    • 引数を省略可能にし、指定しなかった場合はそのデバイスにとって良い値が使われる・・・のが理想
    • とりあえず適当に、省略した場合は0.3秒とかで良さそう
    • 将来的には測定機能をコアに実装して、最適なチャンクサイズが自動適用されると楽しそう
  • その他補足
    • FastAPIのStreamingResponseを使うかどうか
      • これがどういう仕様になってるのか全くわからない。。。(ドキュメントに書いてない)
      • wavファイルがそのまま返せるなどが満たせるならこれで良さそう、試してみるしかなさそう?
      • サクッと仕様が変わりそうなのでe2eのスナップショットテストは絶対書いておきたい。
    • 音声の途中から生成・途中まで生成に対応する場合
      • とりあえずトークだけの場合は考える必要なさそう
      • ソングは再生ヘッドの位置から生成したり、すでに生成されている領域まで生成したいときもある
      • 作る場合は「引数は省略可能」、「省略すると最初から生成する・最後まで生成する」にすれば問題なさそう。
    • ソングの生成結果が冪等ではない件
      • 今の製品版VOICEVOXは、トークは乱数要素がないけど、ソングは実は乱数要素がある
      • なのでもし↑の仕様の API ができても、「途中から生成」したあと「最初から途中まで生成」してもおそらく波形が繋がらない
      • エンジン側ではどうしようもない、コア側に乱数のシード値や乱数をハッシュに対応付ける機能が必要
        • ちなみにコアでも現状どうしようもない
        • 対応するならまず乱数をonnxに入力可能にするところからになる
    • サンプリングレートの変更は不可能で考えると良さそう
      • ストリーミング可能なサンプリングレート変更が必要だけど、手法を知らない
      • AudioQueryのsampling_rateがEngineManifestのdefault_sampling_rateではなかったらエラー返せば良さそう
    • モーフィングも一旦対応なしになりそう
      • トークの場合 F0 推定が必要で、これはファイル全体がないとできない
      • 混合する部分だけストリーミングにすることはできなくはないけど、その前の2ファイルの生成が大半なのであまり意味なさそう

一旦僕が考えたのは以上です!
絶対抜け漏れあるので、かなり気軽にコメントいただければ!!

@sevenc-nanashi
Copy link
Member

sevenc-nanashi commented Nov 27, 2024

FastAPIのStreamingResponse
これがどういう仕様になってるのか全くわからない。。。(ドキュメントに書いてない)

https://www.starlette.io/responses/#streamingresponse
普通にドキュメントがありそう?
AsyncIterableを渡せばいい感じにやってくれそうです。

サンプリングレートの変更は不可能で考えると良さそう
ストリーミング可能なサンプリングレート変更が必要だけど、手法を知らない

こう、求めてるサンプリングレートの1チャンクが44100Hz(コアのサンプリングレートって幾つだっけ...)で何フレームか計算してバッファ的な...(とは言っても音質は落ちそう)

@sabonerune
Copy link
Contributor

FastAPI側のStreamingRespnseのドキュメント
https://fastapi.tiangolo.com/advanced/custom-response/#streamingresponse
(この辺りわざわざ一時ファイルを作らずにレスポンスを返せないか調査したときに見た記憶がある)

Soxrはストリーミングに対応しているようです。
https://python-soxr.readthedocs.io/en/stable/soxr.html#soxr.ResampleStream

soundfileの方がファイルに対する操作しか無いので別の方法を探す必要があると思います。

@Hiroshiba
Copy link
Member

Hiroshiba commented Nov 28, 2024

@sevenc-nanashi
StreamingRespnseの仕様ですが、どういうタイミングでどういうデータが送信されるのかとかがわからないなぁと。
generatorからのデータを一気に送信してくれる気はするのですが、ドキュメントには書かれてない。
あとTransfer-Encoding: chunkedとかで送信してないことも念の為確認したほうが良さそう。

@sabonerune
soxrなるほどです!

そういえばリサンプリングは、途中から生成したあと最初から途中まで生成することも考えて検討が必要かもですね。。。
seakみたいなのができるのがベストだけど、流石に存在しなさそう。(というか信号処理的に無理そうな予感)
まあ相当難しそう。ブラウザとか普通にやってそうだけど、どうやってるんだろう・・・・。

まあリサンプリングはだいぶ後回しにして良さそう!

@sabonerune
Copy link
Contributor

通常のResponsecontentstrbytesである必要があるのに対してStreamingResponseIterableを渡せるので生成や読み込みを遅延できるというだけであまり大きな違いはないはずです。
(しいて言うなら性質上Content-Lengthヘッダーが自動的に設定できないとかでしょうか?)

ストリーミング音声合成はリクエスト側で追加のパラメーター必要だったり音質のトレードオフがあるのでしょうか?
そういったものがなくてaudio/wavで返すなら/synthesisの中身だけ変えたりもできそうな気がします。

(TTSEngine.synthesize_wave()の戻り値をIterable[NDArray]にできるならそれを処理してく感じで…)

@Hiroshiba
Copy link
Member

@Yosshi999 さんが実装してくださったストリーミング合成は音声劣化なしです🙏

置き換える案面白いですね!!
リサンプリングが動かなくなること、content lengthがなくなる気がすること、一括DLじゃなくなること(これは問題ないかも)辺りが破壊的変更かもなのと、あと結構今後apiの仕様に相違を生じさせたくなるかもな気がするので、分けたほうが良い…かもです。(若干自信なし)

@qryxip
Copy link
Member

qryxip commented Nov 30, 2024

置き換える案を考えてみたのですが、素人考えなのですがパラメータを追加し、デフォルトを?format=wav&chunk_size=buffer_size=1みたいにすれば上手くいくのでは…? という気がしました。

@Hiroshiba
Copy link
Member

@qryxip コメントありがとうございます!!

その場合もcontent lengthがつかないという変更はありそうです。
一括で返せる条件のときは今までと同じレスポンスにすればいける…かもですが、ストリームの時のみ必要になりそうな引数が今後出てきそうなので、無理して一緒にせず分けておくのが安定な気はしました。

運用して問題なければ、将来メジャーバージョンアップしたときに統合したいですね!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants