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

ストリーミング処理の対応 #853

Closed
Yosshi999 opened this issue Oct 12, 2024 · 16 comments · Fixed by #854
Closed

ストリーミング処理の対応 #853

Yosshi999 opened this issue Oct 12, 2024 · 16 comments · Fixed by #854

Comments

@Yosshi999
Copy link
Contributor

内容

音声を逐次的に生成する機能を提供し、長い文章の音声を生成する際のレイテンシの短縮をめざす。

関連

Pros 良くなる点

  • レイテンシの短縮:現状の処理時間は生成する音声の長さに比例して増大するため、長い文章を入力したときにかなりの処理時間を要することがある。処理する部分を固定長のチャンクに分け、逐次的に音声を生成することで入力全体が生成されきる前に音声の再生などを行うことを可能にする。

Cons 悪くなる点

  • 機能の複雑化(どこまで凝った実装をするかによる。後述)

実現方法

現在の音声合成処理(synthesis)はaudio queryを入力としてスペクトログラム生成 -> 音声波形生成の順に処理されており、前半と後半の処理時間の比は大体1:10くらいである。また、スペクトログラム生成は内部のモデルの仕様上audio query全体を入力する必要があるが、後半の音声波形生成は十分にマージンを取っていれば任意の位置から任意の長さの音声を生成することができる。

参考:
https://github.com/Yosshi999/streaming_hifigan

そこで、現在 decode として処理されている関数を二つに分け、 generate_full_intermediate 関数によってえられる中間表現(現状ではスペクトログラム)をいったんユーザーに返し、その中間表現と指定区間を入力として音声を生成する render_audio_segment 関数によって最終的な音声を生成する。

実装が必要なもの

  • struct Intermediate : ユーザーにいったん返す中間表現。実態(スペクトログラム)には触れてほしくないが、lengthは後段の生成区間指定などのために参照可能にする必要がある。
  • blocking::Synthesizer::synthesis_intermediate(&self, &AudioQuery, StyleId, &SynthesisOptions) -> Result<Intermediate> : 現在のsynthesis()の途中(decodeの前半部)までを実行する。現在音声が途切れる問題のworkaroundとして前後にパディングがついており、これを音声生成時に取り除くためパディング長をどこかで共有する必要がある。
  • blocking::Synthesizer::render_audio_segment(&self, Intermediate, usize, usize) -> Result<Vec<u8>> : 中間表現から指定した区間の音声を生成する。
  • nonblocking::* も必要?
  • 各種api露出

あるとうれしいもの

  • とくに何も考えなくても音声チャンクが手に入るIterator
  • 理想的なチャンク長や、いまから音声バッファの再生を始めてもいいか判定してくれる関数
    • 処理時間よりも生成される音声長が長い場合(効率>1)、一個めのチャンクができ次第再生を開始してもよいが、効率が低い場合、再生バッファに詰める音声の生成が追い付かずつっかえるため気を付ける必要がある
    • 正直ユーザー側が実装するべきものかもしれない
@Yosshi999
Copy link
Contributor Author

決めなきゃいけないこと

  • struct Intermediateに何を詰めるべきか(workaround paddingの長さ、音声長など)
  • 指定区間の単位(秒数か24kHz or 24k/256Hzで表現されるフレーム数か)
  • 他言語向けapiの設計

@qryxip
Copy link
Member

qryxip commented Oct 12, 2024

名前については、ユーザー向けレベルならstruct Intermediatestruct Audioとかでもよいと思います。というのも"AudioQuery"から作るので。

またintermediateがSynthesizerの参照を保持して、メソッド.render()を生やしてもいいんじゃないかと。
(内部実装レベルではちょっと苦労しそうですが)

struct Intermediatestruct Audioとした場合、synthesis_intermediateの方は…思い付かない。seekable_synthesisとか? (単なる"streaming"というより、任意区間に対して実行できることからRustのSeekのような感じで)

audio: Audio = synth.seekable_synthesis(aq, amaama_zundamon)

wav_part: bytes = audio.render(slice(t1, t2 + 1))

wav_whole: bytes = audio.render(slice(None))

# よい感じに分割したイテレータを作る
for wav_part in audio.render_segments():
    ...

@Yosshi999
Copy link
Contributor Author

Yosshi999 commented Oct 12, 2024

intermediateがSynthesizerの参照を保持して、メソッド.render()を生やし

#854 で組んでみましたがpyo3がまだできていません..
Audioに面倒なライフタイムなどが付いてきているんですがこのままpythonに渡してしまっても大丈夫なんでしょうか

@Yosshi999
Copy link
Contributor Author

error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/latest/class.html#no-lifetime-parameters
   --> crates\voicevox_core_python_api\src\lib.rs:429:29
    |
429 |     pub(crate) struct Audio<'synth> {
    |

Audioの中にsynthesizerの参照を持たせるためにはlifetimeを導入する必要があるが、pythonにはこのままでは渡せない

@Yosshi999
Copy link
Contributor Author

とりあえずrender関数をSynthesizerの中に入れました

@qryxip
Copy link
Member

qryxip commented Oct 12, 2024

とりあえず複雑性を避けた方が議論が円滑になりそうですし、今はそれでよいと思います。

もしやるとしたら、Arcを使ってSynthesizer (#[pyclass]の方)をCloneにし、

     #[pyclass]
+    #[derive(Clone)]
     pub(crate) struct Synthesizer {
-        synthesizer: Closable<
-            voicevox_core::blocking::Synthesizer<voicevox_core::blocking::OpenJtalk>,
-            Self,
-            SingleTasked,
+        synthesizer: Arc<
+            Closable<
+                voicevox_core::blocking::Synthesizer<voicevox_core::blocking::OpenJtalk>,
+                Self,
+                SingleTasked,
+            >,
         >,
     }

ouroborosでライフタイムパラメータを消す、という形になるかと思います。

use ouroboros::self_referencing;

#[pyclass]
#[self_referencing]
struct Audio {
    synthesizer: Synthesizer, // `#[pyclass]`の方

    #[borrows(synthesizer)]
    #[not_covariant]
    inner: voicevox_core::blocking::Audio<'this>, // Open JTalkは要らないので、`&voicevox_core::blocking::Synthesizer`自体ではなく`&Status`を持つようにすれば型引数の`O`は消えるはず
}

(ただ #832 の考えかたからすると"pyclass"丸ごとじゃなくてClosableの中身のRwLockのガードを持つようにすれば一貫性を保てるのですが、そのためにはSendという性質をどうにかして得る必要があって、今このリポジトリで使っているblockingというライブラリのスレッドプールを拝借してそこからasync-channelで…ということになりそう)

[追記] 文脈として、class Synthesizerには__enter____exit__があります。

@qryxip
Copy link
Member

qryxip commented Oct 12, 2024

今のところヒホさん抜きで話が進んでいますが、COREのRust API & Python APIを実装しながら議論する、という今の流れはいいんじゃないかなと思っていることを表明しておきます。

余程複雑なAPIにしない限りはC APIも(ENGINEの)Web APIも後から考えられるはず。
(先日のDiscordでの議論を踏まえても、精々デストラクトがどうのIDがどうのという話に帰着できそう)

@Hiroshiba
Copy link
Member

指定区間の単位(秒数か24kHz or 24k/256Hzで表現されるフレーム数か)

フレーム数が良いと思います!
ソングのときに考えたのですが、秒にすると丸め込み方向とかも考える必要があって、全部フレーム単位にしたほうが良いな~となりました。

struct Intermediateに何を詰めるべきか(workaround paddingの長さ、音声長など)

パッと正解が思いつかないですね。。試行錯誤して作っていく感じになるかも。
必要であればガッツリ考えます!!

パディング、つまり音声を出力するために前後何フレーム余分に中間表現を入力する必要があるかですが、これはAPIには露出させない形を目指せると嬉しそうです。
(つまり必要なパディングの計算などはコアの中で勝手にやってくれる形)

名前については、ユーザー向けレベルならstruct Intermediateはstruct Audioとかでもよいと思います。というのも"AudioQuery"から作るので。

返り値をクラスインスタンスにするの良いですね!!
どっちかというと最終成果物がAudioなので、AudioRendererとかRendererもわかりやすいかもですね!

今のところヒホさん抜きで話が進んでいますが、COREのRust API & Python APIを実装しながら議論する、という今の流れはいいんじゃないかなと思っていることを表明しておきます。

良いと思います!!

@sevenc-nanashi
Copy link
Member

フレーム数単位にするならSynthesisIntermediateに実際のフレームレートを持たせた方がいいと思います、というのも記憶が正しければtts経由だとサンプリングレート取れないはずなので。(あとラッパー作るのも楽になる)

@Hiroshiba
Copy link
Member

フレーム数単位にするならSynthesisIntermediateに実際のフレームレートを持たせた方がいいと思います

これちょっと弱点があって、関数を実行するまでフレームレートがわからないんですよね。
特にUI作ってると先にフレームレートが知りたくなることもあり。

個人的には別のとこに置くのが良いんじゃないかな~~~と思ってます。
VOICEVOX ENGINEリポジトリは、EngineManifestというのを別途作ってframeRateを置いてます。

@qryxip
Copy link
Member

qryxip commented Oct 13, 2024

24kHzから変える予定が無いのなら、グローバルな値として提示してもいいんじゃないかと思います。C APIなら#defineでもよさそう。
(次点は…VVM単位?)

名前については、DEFAULT_SAMPLING_RATEじゃなくてBASE_とかにするか、単なるSAMPLING_RATEにしてもいいかもしれません。frameRateとも統一性が出るんじゃないかと。

@Hiroshiba
Copy link
Member

たしかに、サンプリングレートもフレームレートも一旦固定で良いと思います!
将来的には変わる可能性はあるだろうけど、もしあるとしてもほんとに先になりそう。

@Hiroshiba
Copy link
Member

@Yosshi999 さんのおかげで、コア側でストリーミング生成(render)が可能になってきています!!
ありがとうございます!!!

ちょっと今後VOICEVOX ENGINEで利用する場合、色々考えることがあるのでまとめてみました!!


compatible engineにどう実装するか

  • (VOICEVOX ENGINEはコアAPIを直接使わず、compatible_engine.rsのものを使っている)
    • 主に互換性の理由だけど
  • Synthesizerで露出している API はAudioQueryの入力を想定してる
    • コアC APIにおいて、AudioQueryはjson文字列
  • 一方既存のcompatible_engine.rsのAPIは、ほぼonnxの推論しかしない
    • 入力をTensorで受け取って、出力をTensorで返す
  • AudioQueryを経由する形にするか、ほぼonnxの推論しかしない形にするかが迷いどころ
  • おそらく周囲に揃えるために、後者のほぼonnxの推論しかしないが良さそうに思う
    • AudioQueryを経由するとなぜかその関数だけjson文字列を渡さないといけないようになってしまう
    • あとcompatible_engine.rsからSynthesizerの構造体への依存が発生してしまってちょっとややこしいことになりそう
  • ほぼonnxの推論しかしない形だと、numpy.array相当のものを入出力する形になるので、音声特徴量(internal_state: ndarray::Array2<f32>)が露出する形になってしまう
    • 別にOK!(今までもそうだったので)
    • コアAPIでこの特徴量を露出していなかったのは、保存されると互換性が保てない等の意図があった
    • でもcompatible_engine.rsは正式サポートしていない形になっているので、仮に保存している人がいたとしても動作保証外だから気にしなくて良い
    • VOICEVOX ENGINE側がちょっと頑張ればOK

↑のために実装をどう変えるか

  • precompute_renderとrender関数はコアAPI用で、compatible_engine.rsじゃなさそう
  • なのでcompatible_engine.rs用(≒run_sessionするやつ)を別途作る形が良さそう
  • 実はもうある、generate_full_intermediaterender_audio_segment
  • 1点変更したいとこ、precompute_renderのpadding部分を共通化したい
    • たぶんpadding処理をprecompute_renderからgenerate_full_intermediateに移すと良さそう?
    • precompute_renderはAudioQueryからの翻訳、AudioFeatureへのラップをする感じとか・・・・・?(?)

@Hiroshiba
Copy link
Member

もう1点、これは @Yosshi999 さんがもしという感じなのですが、VOICEVOX ENGINEへの導入に向けた実装にご興味あったりしませんか!!

ちなみにVOICEVOX ENGINEへの導入は3つステップがあって、必要な能力がちょっとずつ変わってくる感じです。

1つが↑でもあげていた、compatible_engine.rsへの実装です。
これはRustのコードを読めればできそう・・・・・・・・・・?????(自信なし)
ただcompatible_engine.rs内にあるAPIの引数などの形が特殊なので、不明な点が多くあるかも。
これは不明で当たり前なので、何でも聞いていただければ!!!

2つ目がVOICEVOX ENGINEからコアのgenerate_full_intermediaterender_audio_segmentをとりあえず使えるようにする点です。
compatible_engine.rsの中身はC APIとして露出するので、Pythonからこれを叩けるようにVOICEVOX ENGINE側を変更する感じです。
たぶんvoicevox_engine/core/core_wrapper.pyの辺りがそうなはず。
↓とかがdecode_forwardの例で、割と直感的かもです。
https://github.com/VOICEVOX/voicevox_engine/blob/fe3eb765bcc724553f1002f076cf0efdbba29d51/voicevox_engine/core/core_wrapper.py#L448-L458
ただTensor周りというか、numpy.array周りに慣れてないとわけわからないことになります。(この点は全く問題なさそう)

3つ目がおそらく一番難しくて、VOICEVOX ENGINEからprecompute_renderrender相当の機能をWEB APIとして実装するとどめの作業です。
voicevox_engine/app/routers/tts_pipeline.pyあたりが実装ポイントです。↓にsynthesisAPIの実装があります。
https://github.com/VOICEVOX/voicevox_engine/blob/fe3eb765bcc724553f1002f076cf0efdbba29d51/voicevox_engine/app/routers/tts_pipeline.py#L264
この実装が実際WEB APIのPOST /synthesisとして実装されます。
これがなんで難しいかというと、WEB APIをどうすればいいかが自明じゃなく、色々設計するところから始めないといけないためです。
どうやったら使いやすくなるのかとか、何が一般的なのかとかを考えつつ、とりあえず作ってみる感じかなと。

もしご興味あれば・・・!!!

qryxip added a commit that referenced this issue Oct 29, 2024
この本文は @qryxip が記述している。

#851 で生まれた`generate_full_intermediate`と`render_audio_segment`を用
いて次の公開APIを作る。`precompute_render`で`AudioFeature`を生成し、
`AudioFeature`と区間指定を引数とした`render`で指定区間のPCMを生成する
形。

- `voicevox_core::blocking::Synthesizer::precompute_render`
- `voicevox_core::blocking::Synthesizer::render`
- `voicevox_core::blocking::AudioFeature`

また`render`で生成したPCMをWAVとして組み立てるため、次の公開APIも作る。

- `voicevox_core::wav_from_s16le`

ただしこのPRで実装するのはRust APIとPython APIのみ。非同期API、C API、
Java APIについては今後実装する。Python APIのtype stubも今後用意する。ま
たテストも今後書く。

Refs: #853

Co-authored-by: Ryo Yamashita <[email protected]>
Co-authored-by: Hiroshiba <[email protected]>
Co-authored-by: Nanashi. <[email protected]>
@Yosshi999
Copy link
Contributor Author

もう1点、これは @Yosshi999 さんがもしという感じなのですが、VOICEVOX ENGINEへの導入に向けた実装にご興味あったりしませんか!!

ちなみにVOICEVOX ENGINEへの導入は3つステップがあって、必要な能力がちょっとずつ変わってくる感じです。

1つが↑でもあげていた、compatible_engine.rsへの実装です。 これはRustのコードを読めればできそう・・・・・・・・・・?????(自信なし) ただcompatible_engine.rs内にあるAPIの引数などの形が特殊なので、不明な点が多くあるかも。 これは不明で当たり前なので、何でも聞いていただければ!!!

2つ目がVOICEVOX ENGINEからコアのgenerate_full_intermediaterender_audio_segmentをとりあえず使えるようにする点です。 compatible_engine.rsの中身はC APIとして露出するので、Pythonからこれを叩けるようにVOICEVOX ENGINE側を変更する感じです。 たぶんvoicevox_engine/core/core_wrapper.pyの辺りがそうなはず。 ↓とかがdecode_forwardの例で、割と直感的かもです。 https://github.com/VOICEVOX/voicevox_engine/blob/fe3eb765bcc724553f1002f076cf0efdbba29d51/voicevox_engine/core/core_wrapper.py#L448-L458 ただTensor周りというか、numpy.array周りに慣れてないとわけわからないことになります。(この点は全く問題なさそう)

3つ目がおそらく一番難しくて、VOICEVOX ENGINEからprecompute_renderrender相当の機能をWEB APIとして実装するとどめの作業です。 voicevox_engine/app/routers/tts_pipeline.pyあたりが実装ポイントです。↓にsynthesisAPIの実装があります。 https://github.com/VOICEVOX/voicevox_engine/blob/fe3eb765bcc724553f1002f076cf0efdbba29d51/voicevox_engine/app/routers/tts_pipeline.py#L264 この実装が実際WEB APIのPOST /synthesisとして実装されます。 これがなんで難しいかというと、WEB APIをどうすればいいかが自明じゃなく、色々設計するところから始めないといけないためです。 どうやったら使いやすくなるのかとか、何が一般的なのかとかを考えつつ、とりあえず作ってみる感じかなと。

もしご興味あれば・・・!!!

やってみます。実装順は上の通りで確定でいいですかね?とりあえずはdecode関数をgenerate_full_intermediaterender_audio_segmentの二つに分けたものを作ればよいでしょうか

@Hiroshiba
Copy link
Member

うおーありがとうございます!!!

実装順は上の通りで確定でいいですかね?

はい、この流れが良いかなと・・・!

とりあえずはdecode関数をgenerate_full_intermediateとrender_audio_segmentの二つに分けたものを作ればよいでしょうか

ですね!!
あ、decode関数は一旦そのまま残していただければ・・・!!

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