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

ストリーミング処理のC API実装 #866

Closed
Yosshi999 opened this issue Nov 1, 2024 · 24 comments · Fixed by #875
Closed

ストリーミング処理のC API実装 #866

Yosshi999 opened this issue Nov 1, 2024 · 24 comments · Fixed by #875

Comments

@Yosshi999
Copy link
Contributor

内容

#853 をC APIのcompatible_engineで対応させる

Pros 良くなる点

ストリーミング処理をvoicevox_engineに実装するための足掛かりになる

Cons 悪くなる点

実現方法

以前の議論:#853 (comment)

現在のdecode_forwardを分割してgenerate_full_intermediaterender_audio_segmentを実装することになるが、一つ問題がある。
中間生成物の音声特徴量(internal_state: ndarray::Array2)が露出することは問題がないが、この配列のwidth (特徴次元数)が未定義であるため、APIを呼び出す側がバッファサイズを設定できない

解決方法:

方針1. spectrogramはrust側に管理してもらう→compatible_engineにステートを持たせてhandleもしくはvoid*でAPIとやりとりする (lockやmemory周りが面倒そう)
方針2. generate_full_intermediateは二回呼んでもらう。一回目はoutput=nullptrでスペクトログラム次元数 or 配列長を返し、二回目でoutputにデータを埋める (二度手間、若干の速度低下)
方針3. スペクトログラム次元数を定数もしくはstyle_idに対する関数のAPIを生やす(モデルに変更が入った時の保守が面倒)
そもそも中間生成物が二次元配列であること自体を隠蔽すべき?そうなると方針1しかなさそう

@Hiroshiba
Copy link
Member

Hiroshiba commented Nov 1, 2024

issue作成ありがとうございます!!

特徴次元数について、かなり特殊になってしまうのですが、ちょっと関数の引数として受け取る形でどうでしょうか・・・?
VOICEVOX ENGINE側から渡してもらう設計です!

↓ご説明です 🙇

実はdecodeへ入力するときに特徴量次元が必要になっています。
音素をonehotベクトルにして入力してるので、2次元配列にするには音素数がです。
どうやって取り扱ってるか調べてみると、compatible_engineの関数の引数で指定する形になっていました!!!

コア側のcompatible_engineの実装

エンジン側からコアを呼ぶ部分の実装
https://github.com/VOICEVOX/voicevox_engine/blob/9506de28639a067cf0540aa1725586d15c9bcf2c/voicevox_engine/core/core_wrapper.py#L725

ちなみに固定値80なのでほんとは定数でもよいのですが、APIは一貫性を持たせて以前のに合わせるのが良さそうなので、この形で行ければと思った次第です。。。。。
なのでVOICEVOX ENGINE側にはマジックナンバー80をcore_wrapper.py辺りに書く感じになるかなと。。。

なんかいびつな実装ですが。。。。。この形でいければ。。。。 🙇
マジックナンバーを実装するワークアラウンド的解決ですが、少なくとも3年この値は変わってないのと、もし変わることがあったら関数を追加する感じで進められれば!!
(その時はスタイルIDを指定して特徴量サイズを得る方針3の方法が良さそう!)

ちょっと3年くらい経ってるので考慮漏れが多くなってしまっていて申し訳ないです 🙇
わからない点・変な点あったらなんでもお聞きください!!!

@qryxip
Copy link
Member

qryxip commented Nov 2, 2024

方針3.でよいと思います。

ちなみにもし方針1.にする場合はこうなるかなと
use std::ptr::NonNull;

use ndarray::Array2;

// この`AudioFeatureInternalState`はポインタでしかやりとりしないので、使う側 (i.e. VOICEVOX ENGINE)
// がサイズやアラインメントを知らなくても全く問題無い
type AudioFeatureInternalState = Array2<f32>;

// SAFETY: voicevox_core_c_apiを構成するライブラリの中に、これと同名のシンボルは存在しない
#[unsafe(no_mangle)]
pub unsafe extern "C" fn generate_full_intermediate(
    // vvvvvvvvvvvvvvvvvv
    length: i64,
    phoneme_size: i64,
    f0: *mut f32,
    phoneme: *mut f32,
    speaker_id: *mut i64,
    // ^^^^^^^^^^^^^^^^^^ ここまで`decode`と同じ
    output: NonNull<Box<AudioFeatureInternalState>>, // ABI的に`*mut *mut AudioFeatureInternalState`として扱ってよく、Cの表現では`**AudioFeatureInternalState`となる
) -> bool {
    let internal_state: AudioFeatureInternalState = todo!();

    output.write_unaligned(internal_state.into());
    true
}

// SAFETY: voicevox_core_c_apiを構成するライブラリの中に、これと同名のシンボルは存在しない
#[unsafe(no_mangle)]
pub unsafe extern "C" fn render_audio_segment(
    // vvvvvvvvv
    length: i64,
    // ^^^^^^^^^ `decode`と同じ
    internal_state: &AudioFeatureInternalState, // ABI的には`*const AudioFeatureInternalState`として扱ってよく、Cの表現では`const *AudioFeatureInternalState`となる
    // vvvvvvvvvvvvvvvvvv
    speaker_id: *mut i64,
    output: *mut f32,
    // ^^^^^^^^^^^^^^^^^^ `decode`と同じ
) -> bool {
    todo!();
}

// SAFETY: voicevox_core_c_apiを構成するライブラリの中に、これと同名のシンボルは存在しない
#[unsafe(no_mangle)]
pub extern "C" fn delete_audio_feature_internal_state(
    internal_state: Box<AudioFeatureInternalState>, // ABI的には`*mut AudioFeatureInternalState`として扱ってよく、Cの表現では`*AudioFeatureInternalState`となる
) {
    // `Box`として扱った時点で、スコープから外れたときにデストラクトされることが決定されるが、
    // 未使用変数警告を黙らせるために明示的に`drop`しておく
    drop(internal_state);
}

@Hiroshiba
Copy link
Member

Hiroshiba commented Nov 2, 2024

@qryxip
最初は特徴量次元数は80固定にして、次元数が複数ありえるようになったら方針3を実装というのはどうでしょう…?👀

(たぶんcompatible_engineは以前のAPIと合わせた方が使い勝手が良くて、特にダメな理由がなければVOICEVOX ENGINEから渡してもらう設計にした方が良いかなーーーと思っており。。。)

@qryxip
Copy link
Member

qryxip commented Nov 2, 2024

あ、方針3.自体を読み違えていました。ヒホさんの案で良いと思います!

@Hiroshiba
Copy link
Member

すみませんありがとうございます 🙇 🙇 🙇

@Yosshi999
Copy link
Contributor Author

Yosshi999 commented Nov 2, 2024

#866 (comment)
これで進めていきます。

もう一つ課題が見つかりまして、render_audio_segment がかなり素朴な実装で、チャンク処理の際のマージン追加をどこかでやらないといけないんですが、

  • compatible engineのc apiの中でやる ( render_audio_segment の引数や実装がsynthesizerのものと異なってくる)
  • voicevox engineでやる (apiユーザーにマージン処理を書かせるのは酷?)

どちらが良いでしょうか。無音パディングはvoicevox engineでやっていたかとおもうのでマージン処理も後者にしてしまったほうが個人的にはいいのかなと思います(rust側のtestでもマージン処理を追加で書くことになりますが..)

@qryxip
Copy link
Member

qryxip commented Nov 2, 2024

VOICEVOX ENGINEでやり、VOICEVOX ENGINEからはprecompute_renderrender相当のAPIを提供する、という話であるという認識でいました。

COREとENGINEで同じような実装をすることになると思いますが、Discordのここ(↓)の前後で議論してた「二重実装」はそのことを指していました。今のところ、仮にcompatible_engineをPyO3に置き換えることになったとしても、この二重実装だけは続けることになるかなと思っています。
https://discord.com/channels/879570910208733277/893889888208977960/1300035516250456065

@qryxip
Copy link
Member

qryxip commented Nov 2, 2024

あ、マージン追加というのを誤読していました。

以下のコードは #854 以前ものですが、これがそのまま「ENGINEから見たCOREのAPI = compatible_engine」の実装になっています。これに従えば、マージン処理はcompatible_engine、というよりfn render_audio_query内にあった方がよいかなと思います。

impl<O> PerformInference for self::Synthesizer<O> {
fn predict_duration(&self, phoneme_vector: &[i64], style_id: StyleId) -> Result<Vec<f32>> {
let (model_id, inner_voice_id) = self.status.ids_for::<TalkDomain>(style_id)?;
let PredictDurationOutput {
phoneme_length: output,
} = self.status.run_session(
model_id,
PredictDurationInput {
phoneme_list: ndarray::arr1(phoneme_vector),
speaker_id: ndarray::arr1(&[inner_voice_id.raw_id().into()]),
},
)?;
let mut output = output.into_raw_vec();
for output_item in output.iter_mut() {
if *output_item < PHONEME_LENGTH_MINIMAL {
*output_item = PHONEME_LENGTH_MINIMAL;
}
}
return Ok(output);
const PHONEME_LENGTH_MINIMAL: f32 = 0.01;
}
fn predict_intonation(
&self,
length: usize,
vowel_phoneme_vector: &[i64],
consonant_phoneme_vector: &[i64],
start_accent_vector: &[i64],
end_accent_vector: &[i64],
start_accent_phrase_vector: &[i64],
end_accent_phrase_vector: &[i64],
style_id: StyleId,
) -> Result<Vec<f32>> {
let (model_id, inner_voice_id) = self.status.ids_for::<TalkDomain>(style_id)?;
let PredictIntonationOutput { f0_list: output } = self.status.run_session(
model_id,
PredictIntonationInput {
length: ndarray::arr0(length as i64),
vowel_phoneme_list: ndarray::arr1(vowel_phoneme_vector),
consonant_phoneme_list: ndarray::arr1(consonant_phoneme_vector),
start_accent_list: ndarray::arr1(start_accent_vector),
end_accent_list: ndarray::arr1(end_accent_vector),
start_accent_phrase_list: ndarray::arr1(start_accent_phrase_vector),
end_accent_phrase_list: ndarray::arr1(end_accent_phrase_vector),
speaker_id: ndarray::arr1(&[inner_voice_id.raw_id().into()]),
},
)?;
Ok(output.into_raw_vec())
}
fn decode(
&self,
length: usize,
phoneme_size: usize,
f0: &[f32],
phoneme_vector: &[f32],
style_id: StyleId,
) -> Result<Vec<f32>> {
let (model_id, inner_voice_id) = self.status.ids_for::<TalkDomain>(style_id)?;
// 音が途切れてしまうのを避けるworkaround処理が入っている
// TODO: 改善したらここのpadding処理を取り除く
const PADDING_SIZE: f64 = 0.4;
let padding_size =
((PADDING_SIZE * DEFAULT_SAMPLING_RATE as f64) / 256.0).round() as usize;
let start_and_end_padding_size = 2 * padding_size;
let length_with_padding = length + start_and_end_padding_size;
let f0_with_padding = make_f0_with_padding(f0, length_with_padding, padding_size);
let phoneme_with_padding = make_phoneme_with_padding(
phoneme_vector,
phoneme_size,
length_with_padding,
padding_size,
);
let GenerateFullIntermediateOutput { spec } = self.status.run_session(
model_id,
GenerateFullIntermediateInput {
f0: ndarray::arr1(&f0_with_padding)
.into_shape([length_with_padding, 1])
.unwrap(),
phoneme: ndarray::arr1(&phoneme_with_padding)
.into_shape([length_with_padding, phoneme_size])
.unwrap(),
speaker_id: ndarray::arr1(&[inner_voice_id.raw_id().into()]),
},
)?;
let RenderAudioSegmentOutput { wave: output } = self
.status
.run_session(model_id, RenderAudioSegmentInput { spec })?;
return Ok(trim_padding_from_output(
output.into_raw_vec(),
padding_size,
));
fn make_f0_with_padding(
f0_slice: &[f32],
length_with_padding: usize,
padding_size: usize,
) -> Vec<f32> {
// 音が途切れてしまうのを避けるworkaround処理
// 改善したらこの関数を削除する
let mut f0_with_padding = Vec::with_capacity(length_with_padding);
let padding = vec![0.0; padding_size];
f0_with_padding.extend_from_slice(&padding);
f0_with_padding.extend_from_slice(f0_slice);
f0_with_padding.extend_from_slice(&padding);
f0_with_padding
}
fn make_phoneme_with_padding(
phoneme_slice: &[f32],
phoneme_size: usize,
length_with_padding: usize,
padding_size: usize,
) -> Vec<f32> {
// 音が途切れてしまうのを避けるworkaround処理
// 改善したらこの関数を削除する
let mut padding_phoneme = vec![0.0; phoneme_size];
padding_phoneme[0] = 1.0;
let padding_phoneme_len = padding_phoneme.len();
let padding_phonemes: Vec<f32> = padding_phoneme
.into_iter()
.cycle()
.take(padding_phoneme_len * padding_size)
.collect();
let mut phoneme_with_padding =
Vec::with_capacity(phoneme_size * length_with_padding);
phoneme_with_padding.extend_from_slice(&padding_phonemes);
phoneme_with_padding.extend_from_slice(phoneme_slice);
phoneme_with_padding.extend_from_slice(&padding_phonemes);
phoneme_with_padding
}
fn trim_padding_from_output(mut output: Vec<f32>, padding_f0_size: usize) -> Vec<f32> {
// 音が途切れてしまうのを避けるworkaround処理
// 改善したらこの関数を削除する
let padding_sampling_size = padding_f0_size * 256;
output
.drain(padding_sampling_size..output.len() - padding_sampling_size)
.collect()
}
}
}

(というか今、compatible_engineからdecode呼んだらパディング処理すっぽかされるのでは…?)

@Yosshi999
Copy link
Contributor Author

compatible_engineからdecode呼んだらパディング処理すっぽかされる

たしかにそうですね
#854 をrevertしないといけないのかな...

@qryxip
Copy link
Member

qryxip commented Nov 2, 2024

丸ごとrevertよりは、パディング処理とマージン処理を移動というのがよいかなと。というのも #854 ではパブリックAPIの形も決めてたので。
(あと #865 をマージしちゃったので、そこそこ大きくコンフリクトしそう)

@Yosshi999
Copy link
Contributor Author

別PRで修正します

@Yosshi999
Copy link
Contributor Author

#867

@Yosshi999
Copy link
Contributor Author

Yosshi999 commented Nov 2, 2024

ちょっとこんがらがってきたんですが、もともと

  • Synthesizer::decode ... 無音パディングを付加
  • Synthesizer::synthesis ... Synthesizer::decode を利用
  • extern "C" fn decode_forward ... Synthesizer::decode をそのまま利用

だったものに対して #854

  • Synthesizer::generate_full_intermediate ... 無音パディングなしで中間物生成
  • Synthesizer::render_audio_segment ... マージンなしで与えられた音声特徴全体で音声生成
  • Synthesizer::decode ... Synthesizer::generate_full_intermediateSynthesizer::render_audio_segment を利用
  • Synthesizer::precompute_render ... 無音パディングを付加して Synthesizer::generate_full_intermediate
  • Synthesizer::render ... マージンを付けて&指定された区間で Synthesizer::render_audio_segment (+暗黙的にパディングが除去される)
  • Synthesizer::synthesis ... Synthesizer::precompute_renderSynthesizer::render を利用
  • extern "C" fn decode_forward ... Synthesizer::decode をそのまま利用

になっていたんですが、これを

  • Synthesizer::generate_full_intermediate ... 無音パディングを付加して中間物生成
  • Synthesizer::render_audio_segment ... マージンを付けて&指定された区間で音声生成
  • Synthesizer::decode ... Synthesizer::generate_full_intermediateSynthesizer::render_audio_segment を利用
  • Synthesizer::precompute_render ... Synthesizer::generate_full_intermediate を利用
  • Synthesizer::render ... Synthesizer::render_audio_segment を利用
  • Synthesizer::synthesis ... Synthesizer::precompute_renderSynthesizer::render を利用
  • extern "C" fn decode_forward ... Synthesizer::decode をそのまま利用
  • extern "C" fn generate_full_intermediate ... Synthesizer::generate_full_intermediate をそのまま利用
  • extern "C" fn render_audio_segment ... Synthesizer::render_audio_segment をそのまま利用

とすればいいですかね?無音パディング幅をどこに記録するか問題になりそうですが、面倒なのでハードコーディングしてしまったほうがいいですかね

@qryxip
Copy link
Member

qryxip commented Nov 2, 2024

分類については私もその認識です。

ただrender_audio_segmentのシグネチャを今思いだしたのですが、そうか、切り取った後のspecを入力に取るんでしたね。うーん。(spec丸ごと, start, end, style_id)にしちゃってもよいのかも?どうせ他のやつもONNXのシグネチャとは微妙に違ってるし。

@Hiroshiba
Copy link
Member

Hiroshiba commented Nov 3, 2024

すみません遅くなりました!!

@Yosshi999 さんのまとめめちゃくちゃ参考になりました!!!!
ソースコード読みながら見たのですが、自分の理解と完全に一致しました!!!!

僕もお2人と一緒の意見で、spec, start, end引数をrender_audio_segmentが受け取る(Synthesizer内のもextern "C" fnも)のが良さそうに感じてます!
@Yosshi999 さんの仰ってる

無音パディング幅をどこに記録するか問題になりそう

これをどうしようか迷ってます。。。
ちょっとまだ考えきれてないんですが、アイデア思いついたんで提案まで 🙇


Synthesizer::generate_full_intermediateを、無音パディングを追加して中間物を生成したあとrenderのマージン分だけ残して返すというのはどうでしょう・・・・・・・・?

整理すると、generate_full_intermediateの処理がこんな感じ:

  1. 0.4(秒)*93.75(フレームレート)=37.5→37?フレームの無音パディングをする
  2. 生成する。元のフレーム長+37*2の中間物ができる
  3. renderのマージン分(14フレーム)だけ残す。両端37-14ずつ短くする。
  4. returnする。

こうすればrender_audio_segment側は無音paddingの除去を考えず、与えられたspecをそのままrenderすれば良くなるかなと!
(もちろん、14*2分マージンされたspecを渡すようにしないとですが)

これを @Yosshi999 さんの表に適用してみるとこうなるかな~と思ってます。どっか間違ってるかも。。。
(いまのmainブランチからの実装の変更部分を訂正線と太字で書いてます)

種類 関数 説明
Synthesizer generate_full_intermediate 無音パディングを付加して中間物生成、マージン込みのを返す
Synthesizer render_audio_segment マージンなしで与えられた音声特徴全体で音声生成
Synthesizer decode Synthesizer::generate_full_intermediateSynthesizer::render_audio_segment を利用
Synthesizer precompute_render 無音パディングを付加して Synthesizer::generate_full_intermediate
Synthesizer render マージンを付けて&指定された区間で Synthesizer::render_audio_segment (+暗黙的にパディングが除去される)
Synthesizer synthesis Synthesizer::precompute_renderSynthesizer::render を利用
extern "C" decode_forward Synthesizer::decode をそのまま利用
extern "C" generate_full_intermediate Synthesizer::generate_full_intermediate をそのまま利用
extern "C" render_audio_segment Synthesizer::render_audio_segment をそのまま利用

この形だと、render_audio_segmentはspecだけ与える形でいけて、かつVOICEVOX ENGINE側はパディングを意識しなくてもいいはず!
ただVOICEVOX ENGINE側にマージン幅14はマジックナンバーとして必要そう。
compatible_enginerender_audio_segmentの引数でマージン幅を指定可能にするかどうか迷いどころかも・・・?

@Yosshi999
Copy link
Contributor Author

マージン幅やパディング幅はvoicevox_coreにマジックナンバーとして埋め込んでしまって、voicevox_engineやAPIユーザーには全く意識させないのがいいのかなと思います。中間生成物をユーザーがいじるのは完全に保証対象外にしてしまい、spec全体, start, endの組でAPIとやり取りするのが一番楽そう

無音パディング幅をどこに記録するかについてもマジックナンバーにしてしまえば問題なくなります

@Hiroshiba
Copy link
Member

たしかにと思ったのですが、voicevox engine側はメモリ確保が必要なので、少なくともprecompute用のマジックナンバーは必要になるかもです!!

@qryxip
Copy link
Member

qryxip commented Nov 4, 2024

一応、方針1. (void*で扱う)だとマジックナンバーはCORE内に完全に閉じると思います。その場合デストラクトをどこでするかという問題が起きますが、__del__とかでよさそう。
#866 (comment)

@Hiroshiba
Copy link
Member

Hiroshiba commented Nov 4, 2024

AudioFeatureをcompatible_engine側で持っておく感じですよね。
今までcompatible_engineはdelete用の関数が不要な設計だったので、まあちょっと不揃い感はあるな〜くらい。
(これ勘違いでdelete用の関数が既存なら、その方針でも全然良さそう)

うーんどれが良いかな〜〜〜

ほんとになんとなくなんですが、specを毎回受け渡すのは、実装によってはメモリコピーが結構発生しそうなので避け調子が良い気がしてます。
もし長さが30秒の場合30秒 * 93.75フレーム/秒 * 80特徴量 * 4バイト = 0.9MBで、仮に0.2秒ごとに生成したいとなると実装によってはスペック高くない環境で課題になりえるかも・・・?くらいの気持ち。
まあメモリコピーしないように実装すれば良いのですが、考えないといけないことが増えるので避けときたいかもくらい!

@qryxip さんのBox使う形式はdelete関数が必要になるのでVOICEVOX ENGINE側の実装が多少ややこしくなるかも、くらい。
一方で僕の案は、Python側で帳尻を合わせる実装を書かなくちゃいけない。あとマジックナンバーが1つ漏れる。(今までに何個も漏れてるけど)

メモリコピーが発生しないように意識して実装するか、delete関数を召喚するか、spec範囲の計算をVOICEVOX ENGINE側でもやるか、どれにしますかねって感じかなと!!!

個人的には、これまでもVOICEVOX ENGINE側に前処理書いてる(こういうのとか)のと、あとメモリコピー意識とdelete関数召喚は避けたい気持ちもあり、まあ今回もVOICEVOX ENGINE側でコード書く感じが良いのかな〜とちょっと思ってます!

@qryxip
Copy link
Member

qryxip commented Nov 4, 2024

メモリコピー

メモリについてだいぶ齟齬が発生しかけてる気がするので私の考えを述べておくと、#687 をやることを前提にした場合、どの方法であっても分割部分のコピーは避けられるかなと思っています。何故ならRustのndarrayにもNumPyにもviewという概念があるので
(ちなみに #687 をやるための準備は #865 で終わっているので、ort::Session::run_asyncを使うだけだと思います)

[追記] viewというか、specを時間方向に切り分けたものですね。NumPyで持つ場合だと、viewそのままだと多分話がややこしい

ENGINE側でNumPyを持つ場合はgenerate_full_intermediateの出力 (spec全体)を+1回コピーすることになり、CORE側にspecを閉じ込める場合はコピー回数は+0回になるかなと思ってます。

delete関数

__del__というのはGC上のファイナライザを意図していました。ただもしかするとそもそもGCじゃなくて、ユーザーのWeb APIリクエストでdeleteすることになるのでは?という気もしています。というのもユーザーがAudioFeatureを「使い終わ」ったタイミングがこちら側からわからないかもしれないんじゃないかと。

@Hiroshiba
Copy link
Member

#687 をやることを前提にした場合、どの方法であっても分割部分のコピーは避けられるかなと思っています。

なるほどです。
まあこの辺り色々結局考える必要があるのを避けられる、ってのがメリットかなぁと!

ただもしかするとそもそもGCじゃなくて、ユーザーのWeb APIリクエストでdeleteすることになるのでは?という気もしています。

これはちょっと読み切れてないんですが、おそらくWEBリクエストのセッションが切れたら破棄が手っ取り早いそうかな~~~~と思ってます。
まあどうなるにせよ、Python側のAudioFeatureの実装は想像つきます。
ちなみに実はVOICEVOX ENGINEは__del__を使ったことがないはず。
https://github.com/search?q=repo%3AVOICEVOX%2Fvoicevox_engine%20__del__&type=code


ただまあもうプルリクエスト作ってくださってるので、あとはcompatible_engine.rsの実装と、PythonのVOICEVOX ENGINEの実装を想像しつつ、どれがいいか選ぶ感じかな~~~と。

  • specをまるまる全部受け渡しし、render_audio_segmentのシグネチャを(spec, start, end)にする
    • 🤔 メモリコピーが発生しないように意識して実装する
    • 🤔 メモリ確保のため、VOICEVOX ENGINE側にパディング分のマジックナンバーが必要
  • AudioFeatureのIDを受け渡しし、render_audio_segmentのシグネチャを(AudioFeatureId, start, end)にする
    • 🤔 他の API と不揃いになる
    • 🤔 delete関数が増える
  • render_audio_segmentのシグネチャを(spec)にする
    • 🤔 今のプルリクエストを変える必要がある
    • 🤔 範囲計算のため、VOICEVOX ENGINE側にマージン分のマジックナンバーが必要
    • 🤔 spec範囲の計算をVOICEVOX ENGINE側でもやる

個人的には「render_audio_segmentのシグネチャを(spec)にする」が設計で考えることが少ないので(実装は考えることあるけど)、これが一番丸いんじゃないかな~~~と思ってます。
とりあえず実装してみて、Python側も作っちゃうのが良いのかも!!

@qryxip
Copy link
Member

qryxip commented Nov 5, 2024

render_audio_segmentのシグネチャを(spec)にする

一貫性で言うと、「ONNXの入出力を補正する役割はCORE側に持たせる」というのが方針であるという理解を私はしていました。例えばソングで言うと #749 という話もあったと思います。

あとMARGIN=14については、未来永劫…とは言わずとも数年は変わらないとみてよいのか、という点を考慮した方がよいんじゃないかと思っています。


ところでパディング分のマジックナンバーですが、delete関数が駄目ならコールバックでCOREからENGINEに伝えるという手もあると思います。ctypesなら楽々できるはず。イメージとしては次のような感じ:

use std::slice;

/// # Safety
///
/// * ∀n, the pointer that `out(n)` returns must be null or valid as Rust's `&mut [f32; n]`.
#[unsafe(no_mangle)] // SAFETY: there is no other global function of this name
pub unsafe extern "C" fn fill_array(out: extern "C" fn(usize) -> *mut f32) -> bool {
    const SIZE: usize = 5;

    let out = out(SIZE);
    if out.is_null() {
        return false; // failure
    }
    let out = unsafe { slice::from_raw_parts_mut(out, SIZE) }; // SAFETY: the safety requirement of this function
    out.fill(42.);
    true // success
}
import sys
from ctypes import CDLL, CFUNCTYPE, c_bool, c_size_t

import numpy as np

lib = CDLL(PATH_TO_LIBRARY)

buf = np.array([], dtype=np.float32)
print(f"{buf=}", file=sys.stderr)

Out = CFUNCTYPE(c_size_t, c_size_t)

@Out
def out(size: int) -> int:
    try:
        buf.resize((size,))
        print(f"{buf=}", file=sys.stderr)
        return buf.ctypes.data
    except:
        return 0

lib.fill_array.argtypes = (Out,)
lib.fill_array.restype = c_bool

result = lib.fill_array(out)
print(f"{result=}", file=sys.stderr)
print(f"{buf=}", file=sys.stderr)
buf=array([], dtype=float32)
buf=array([0., 0., 0., 0., 0.], dtype=float32)
result=True
buf=array([42., 42., 42., 42., 42.], dtype=float32)

@Hiroshiba
Copy link
Member

@qryxip

一貫性で言うと、「ONNXの入出力を補正する役割はCORE側に持たせる」というのが方針であるという理解を私はしていました。

たーーーしかに!!

まーーーー、compatible_engine.rsは「onnxの入出力の値を補正するけど、データの形を変えるのはエンジン側がやる」という切り分けになる・・・かもです・・・!
ぶっちゃけこれは、ENGINEの機能はPythonに持たせたほうが圧倒的に楽という過去に引っ張られてるので、今の最適解は違っているかもしれません。

ちょっと申し訳ないのですが、今回はこの形で・・・ 🙇 🙇 🙇
VOICEVOX ENGINE実装の際に明らかにダメな点があったらまた再考できれば・・・ 🙇

将来VOICEVOX ENGINEがPython API使うようになる場合、compatible engineを使うかSynthesizerを使うか分かりませんが、前者の場合はの再設計しても良いかもです。
たぶんNdarrayは全部隠蔽しちゃうのが良い気がする!!

あとMARGIN=14については、未来永劫…とは言わずとも数年は変わらないとみてよいのか、という点を考慮した方がよいんじゃないかと思っています。

ほぼ変わらない気がしますが、数年以内に変わる可能性はゼロではないかもしれません。
将来変えられるように、引数でmarginを与えられるようにしておいた方が良さそう・・・!!

コールバックでCOREからENGINEに伝える

本当だ、結構簡単ですね。
まあちょっと何が起こるかわからないので、コールバックは避け調子のが無難かもと感じました。
必要になったらget関数が良いかなと!

@qryxip
Copy link
Member

qryxip commented Nov 5, 2024

ちょっと申し訳ないのですが、今回はこの形で・・・ 🙇 🙇 🙇
VOICEVOX ENGINE実装の際に明らかにダメな点があったらまた再考できれば・・・ 🙇

将来VOICEVOX ENGINEがPython API使うようになる場合、compatible engineを使うかSynthesizerを使うか分かりませんが、前者の場合はの再設計しても良いかもです。
たぶんNdarrayは全部隠蔽しちゃうのが良い気がする!!

なるほど。まあ #867 はとりあえず今のまま進めた方がよさそうですね。

qryxip pushed a commit that referenced this issue Nov 26, 2024
この本文は @qryxip が記述している。

ストリーミング処理を`compatible_engine`に実装する。testcaseとして一括変
換・ストリーム変換の二つの生成結果を追加し、元の生成音声と比較して十分近
いことを確かめた。

`render_audio_segment`には将来の互換性のため、未使用引数
`int64_t margin_width`を入れる。
#875 (comment)

またRust APIの出力サイズをチェックしてパニックする仕組みも入れる。これに
ついては他の関数にも後で導入することとする。
#875 (comment)

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

Successfully merging a pull request may close this issue.

3 participants