VOICEVOX / voicevox_core

無料で使える中品質なテキスト読み上げソフトウェア、VOICEVOXのコア
https://voicevox.hiroshiba.jp/
MIT License
864 stars 117 forks source link

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

Open Yosshi999 opened 1 day ago

Yosshi999 commented 1 day ago

内容

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

関連

Pros 良くなる点

Cons 悪くなる点

実現方法

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

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

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

実装が必要なもの

あるとうれしいもの

Yosshi999 commented 1 day ago

決めなきゃいけないこと

qryxip commented 23 hours ago

名前については、ユーザー向けレベルなら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 commented 21 hours ago

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

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

Yosshi999 commented 20 hours ago
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 commented 19 hours ago

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

qryxip commented 18 hours ago

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

もしやるとしたら、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 commented 14 hours ago

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

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

Hiroshiba commented 12 hours ago

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

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

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

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

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

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

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

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

良いと思います!!

sevenc-nanashi commented 12 hours ago

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

Hiroshiba commented 11 hours ago

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

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

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

qryxip commented 3 hours ago

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

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

Hiroshiba commented 2 hours ago

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