diff --git a/Cargo.lock b/Cargo.lock index 7d70c15b8..53117ce5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -166,6 +166,18 @@ dependencies = [ "yansi", ] +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-compression" version = "0.4.6" @@ -179,6 +191,34 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.57" @@ -203,6 +243,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atty" version = "0.2.14" @@ -354,6 +400,19 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bstr" version = "1.2.0" @@ -695,6 +754,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82a90734b3d5dcf656e7624cca6bce9c3a90ee11f900e80141a7427ccfb3d317" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.15.4" @@ -1253,6 +1321,27 @@ dependencies = [ "libc", ] +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "eyre" version = "0.6.8" @@ -2637,9 +2726,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -2647,6 +2736,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand 2.0.1", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -4243,6 +4343,7 @@ name = "voicevox_core" version = "0.0.0" dependencies = [ "anyhow", + "async-fs", "async_zip", "camino", "const_format", @@ -4254,7 +4355,9 @@ dependencies = [ "educe", "enum-map", "fs-err", - "futures", + "futures-io", + "futures-lite", + "futures-util", "heck", "humansize", "indexmap 2.0.0", @@ -4264,7 +4367,6 @@ dependencies = [ "open_jtalk", "ouroboros", "pretty_assertions", - "rayon", "ref-cast", "regex", "rstest", @@ -4283,7 +4385,6 @@ dependencies = [ "voicevox-ort", "voicevox_core_macros", "windows", - "zip", ] [[package]] @@ -4303,7 +4404,6 @@ dependencies = [ "derive-getters", "duct", "easy-ext", - "futures", "inventory", "itertools 0.10.5", "libc", diff --git a/Cargo.toml b/Cargo.toml index d72625c5f..3a2fffb01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ anstream = { version = "0.5.0", default-features = false } anstyle-query = "1.0.0" anyhow = "1.0.65" assert_cmd = "2.0.8" +async-fs = "2.1.2" async_zip = "=0.0.16" bindgen = "0.69.4" binstall-tar = "0.4.39" @@ -33,10 +34,10 @@ enum-map = "3.0.0-beta.1" eyre = "0.6.8" flate2 = "1.0.25" fs-err = "2.11.0" -futures = "0.3.26" futures-core = "0.3.25" futures-util = "0.3.25" futures-lite = "2.2.0" +futures-io = "0.3.28" heck = "0.4.1" humansize = "2.1.2" indexmap = "2.0.0" diff --git a/crates/voicevox_core/Cargo.toml b/crates/voicevox_core/Cargo.toml index 8cb2b1cfc..74feebb4b 100644 --- a/crates/voicevox_core/Cargo.toml +++ b/crates/voicevox_core/Cargo.toml @@ -16,6 +16,7 @@ link-onnxruntime = [] [dependencies] anyhow.workspace = true +async-fs.workspace = true async_zip = { workspace = true, features = ["deflate"] } camino.workspace = true const_format.workspace = true @@ -27,14 +28,15 @@ easy-ext.workspace = true educe.workspace = true enum-map.workspace = true fs-err = { workspace = true, features = ["tokio"] } -futures.workspace = true +futures-io.workspace = true +futures-lite.workspace = true +futures-util = { workspace = true, features = ["io"] } indexmap = { workspace = true, features = ["serde"] } itertools.workspace = true jlabel.workspace = true ndarray.workspace = true open_jtalk.workspace = true ouroboros.workspace = true -rayon.workspace = true ref-cast.workspace = true regex.workspace = true serde = { workspace = true, features = ["derive", "rc"] } @@ -49,7 +51,6 @@ tracing.workspace = true uuid = { workspace = true, features = ["v4", "serde"] } voicevox-ort = { workspace = true, features = ["download-binaries", "__init-for-voicevox"] } voicevox_core_macros = { path = "../voicevox_core_macros" } -zip.workspace = true [dev-dependencies] heck.workspace = true diff --git a/crates/voicevox_core/src/asyncs.rs b/crates/voicevox_core/src/asyncs.rs new file mode 100644 index 000000000..7bbabbb06 --- /dev/null +++ b/crates/voicevox_core/src/asyncs.rs @@ -0,0 +1,82 @@ +//! 非同期操作の実装の切り替えを行う。 +//! +//! 「[ブロッキング版API]」と「[非同期版API]」との違いはここに集約される +//! …予定。現在は[`crate::voice_model`]のみで利用している。 +//! +//! # Motivation +//! +//! [blocking]クレートで駆動する非同期処理はランタイムが無くても動作する。そのため非同期版APIを +//! もとにブロッキング版APIを構成することはできる。しかし将来WASMビルドすることを考えると、スレッド +//! がまともに扱えないため機能しなくなってしまう。そのためWASM化を見越したブロッキング版APIのため +//! に[`SingleTasked`]を用意している。 +//! +//! [ブロッキング版API]: crate::blocking +//! [非同期版API]: crate::tokio +//! [blocking]: https://docs.rs/crate/blocking + +use std::{ + io::{self, Read as _, Seek as _, SeekFrom}, + path::Path, + pin::Pin, + task::{self, Poll}, +}; + +use futures_io::{AsyncRead, AsyncSeek}; + +pub(crate) trait Async: 'static { + async fn open_file(path: impl AsRef) -> io::Result; +} + +/// エグゼキュータが非同期タスクの並行実行をしないことを仮定する、[`Async`]の実装。 +/// +/// [ブロッキング版API]用。 +/// +/// # Performance +/// +/// `async`の中でブロッキング操作を直接行う。そのためTokioやasync-stdのような通常の非同期ランタイム +/// 上で動くべきではない。 +/// +/// [ブロッキング版API]: crate::blocking +pub(crate) enum SingleTasked {} + +impl Async for SingleTasked { + async fn open_file(path: impl AsRef) -> io::Result { + return std::fs::File::open(path).map(BlockingFile); + + struct BlockingFile(std::fs::File); + + impl AsyncRead for BlockingFile { + fn poll_read( + mut self: Pin<&mut Self>, + _: &mut task::Context<'_>, + buf: &mut [u8], + ) -> Poll> { + Poll::Ready(self.0.read(buf)) + } + } + + impl AsyncSeek for BlockingFile { + fn poll_seek( + mut self: Pin<&mut Self>, + _: &mut task::Context<'_>, + pos: SeekFrom, + ) -> Poll> { + Poll::Ready(self.0.seek(pos)) + } + } + } +} + +/// [blocking]クレートで駆動する[`Async`]の実装。 +/// +/// [非同期版API]用。 +/// +/// [blocking]: https://docs.rs/crate/blocking +/// [非同期版API]: crate::tokio +pub(crate) enum BlockingThreadPool {} + +impl Async for BlockingThreadPool { + async fn open_file(path: impl AsRef) -> io::Result { + async_fs::File::open(path).await + } +} diff --git a/crates/voicevox_core/src/future.rs b/crates/voicevox_core/src/future.rs new file mode 100644 index 000000000..4ddbf3303 --- /dev/null +++ b/crates/voicevox_core/src/future.rs @@ -0,0 +1,16 @@ +use std::future::Future; + +use easy_ext::ext; + +/// `futures_lite::future::block_on`を、[pollster]のように`.block_on()`という形で使えるようにする。 +/// +/// [pollster]: https://docs.rs/crate/pollster +#[ext(FutureExt)] +impl F { + pub(crate) fn block_on(self) -> Self::Output + where + Self: Sized, + { + futures_lite::future::block_on(self) + } +} diff --git a/crates/voicevox_core/src/infer/domains.rs b/crates/voicevox_core/src/infer/domains.rs index 687550399..5225f2ec3 100644 --- a/crates/voicevox_core/src/infer/domains.rs +++ b/crates/voicevox_core/src/infer/domains.rs @@ -1,14 +1,61 @@ mod talk; +use educe::Educe; +use serde::{Deserialize, Deserializer}; + pub(crate) use self::talk::{ DecodeInput, DecodeOutput, PredictDurationInput, PredictDurationOutput, PredictIntonationInput, PredictIntonationOutput, TalkDomain, TalkOperation, }; +#[derive(Educe)] +// TODO: `bounds`に`V: ?Sized`も入れようとすると、よくわからない理由で弾かれる。最新版のeduce +// でもそうなのか?また最新版でも駄目だとしたら、弾いている理由は何なのか? +#[educe(Clone(bound = "V: InferenceDomainMapValues, V::Talk: Clone"))] pub(crate) struct InferenceDomainMap { pub(crate) talk: V::Talk, } +impl InferenceDomainMap<(T,)> { + pub(crate) fn each_ref(&self) -> InferenceDomainMap<(&T,)> { + let talk = &self.talk; + InferenceDomainMap { talk } + } + + pub(crate) fn map T2>( + self, + fs: InferenceDomainMap<(Ft,)>, + ) -> InferenceDomainMap<(T2,)> { + let talk = (fs.talk)(self.talk); + InferenceDomainMap { talk } + } +} + +impl InferenceDomainMap<(Result,)> { + pub(crate) fn collect(self) -> Result, E> { + let talk = self.talk?; + Ok(InferenceDomainMap { talk }) + } +} + +impl<'de, V: InferenceDomainMapValues + ?Sized> Deserialize<'de> for InferenceDomainMap +where + V::Talk: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let Repr { talk } = Repr::deserialize(deserializer)?; + return Ok(Self { talk }); + + #[derive(Deserialize)] + struct Repr { + talk: T, + } + } +} + pub(crate) trait InferenceDomainMapValues { type Talk; } diff --git a/crates/voicevox_core/src/lib.rs b/crates/voicevox_core/src/lib.rs index 94ccc0d5a..dad702cc6 100644 --- a/crates/voicevox_core/src/lib.rs +++ b/crates/voicevox_core/src/lib.rs @@ -48,10 +48,12 @@ const _: () = { ); }; +mod asyncs; mod devices; /// cbindgen:ignore mod engine; mod error; +mod future; mod infer; mod macros; mod manifest; diff --git a/crates/voicevox_core/src/manifest.rs b/crates/voicevox_core/src/manifest.rs index 4460f10bf..203fc76a9 100644 --- a/crates/voicevox_core/src/manifest.rs +++ b/crates/voicevox_core/src/manifest.rs @@ -7,10 +7,14 @@ use std::{ use derive_getters::Getters; use derive_more::Deref; use derive_new::new; +use macros::IndexForFields; use serde::{de, Deserialize, Deserializer, Serialize}; use serde_with::{serde_as, DisplayFromStr}; -use crate::{StyleId, VoiceModelId}; +use crate::{ + infer::domains::{InferenceDomainMap, TalkOperation}, + StyleId, VoiceModelId, +}; #[derive(Clone)] struct FormatVersionV1; @@ -65,26 +69,31 @@ impl Display for InnerVoiceId { } } -#[derive(Deserialize, Getters, Clone)] +#[derive(Deserialize, Getters)] pub struct Manifest { #[expect(dead_code, reason = "現状はバリデーションのためだけに存在")] vvm_format_version: FormatVersionV1, pub(crate) id: VoiceModelId, metas_filename: String, #[serde(flatten)] - domains: ManifestDomains, + domains: InferenceDomainMap, } -#[derive(Deserialize, Clone)] -pub(crate) struct ManifestDomains { - pub(crate) talk: Option, -} +pub(crate) type ManifestDomains = (Option,); -#[derive(Deserialize, Clone)] +#[derive(Deserialize, IndexForFields)] +#[cfg_attr(test, derive(Default))] +#[index_for_fields(TalkOperation)] pub(crate) struct TalkManifest { - pub(crate) predict_duration_filename: String, - pub(crate) predict_intonation_filename: String, - pub(crate) decode_filename: String, + #[index_for_fields(TalkOperation::PredictDuration)] + pub(crate) predict_duration_filename: Arc, + + #[index_for_fields(TalkOperation::PredictIntonation)] + pub(crate) predict_intonation_filename: Arc, + + #[index_for_fields(TalkOperation::Decode)] + pub(crate) decode_filename: Arc, + #[serde(default)] pub(crate) style_id_to_inner_voice_id: StyleIdToInnerVoiceId, } diff --git a/crates/voicevox_core/src/voice_model.rs b/crates/voicevox_core/src/voice_model.rs index 48477256c..ac49d2cdb 100644 --- a/crates/voicevox_core/src/voice_model.rs +++ b/crates/voicevox_core/src/voice_model.rs @@ -2,24 +2,33 @@ //! //! VVM ファイルの定義と形式は[ドキュメント](../../../docs/vvm.md)を参照。 -use anyhow::anyhow; +use std::{ + marker::PhantomData, + path::{Path, PathBuf}, + sync::Arc, +}; + +use anyhow::{anyhow, Context as _}; use derive_more::From; use easy_ext::ext; -use enum_map::EnumMap; +use enum_map::{enum_map, EnumMap}; +use futures_io::{AsyncBufRead, AsyncSeek}; +use futures_util::future::{OptionFuture, TryFutureExt as _}; use itertools::Itertools as _; +use ouroboros::self_referencing; use serde::Deserialize; use uuid::Uuid; use crate::{ + asyncs::Async, error::{LoadModelError, LoadModelErrorKind, LoadModelResult}, infer::{ - domains::{TalkDomain, TalkOperation}, + domains::{InferenceDomainMap, TalkDomain, TalkOperation}, InferenceDomain, }, - manifest::{Manifest, ManifestDomains, StyleIdToInnerVoiceId}, + manifest::{Manifest, ManifestDomains, StyleIdToInnerVoiceId, TalkManifest}, SpeakerMeta, StyleMeta, StyleType, VoiceModelMeta, }; -use std::path::{Path, PathBuf}; /// [`VoiceModelId`]の実体。 /// @@ -51,11 +60,238 @@ impl VoiceModelId { } } +#[self_referencing] +struct Inner { + header: VoiceModelHeader, + + #[borrows(header)] + #[not_covariant] + inference_model_entries: InferenceDomainMap>, + + // `_marker`とすると、`borrow__marker`のような名前のメソッドが生成されて`non_snake_case`が + // 起動してしまう + marker: PhantomData A>, +} + +impl Inner { + async fn from_path(path: impl AsRef) -> crate::Result { + const MANIFEST_FILENAME: &str = "manifest.json"; + + let path = path.as_ref(); + + let error = |context, source| LoadModelError { + path: path.to_owned(), + context, + source: Some(source), + }; + + let mut zip = A::open_zip(path) + .await + .map_err(|source| error(LoadModelErrorKind::OpenZipFile, source))?; + + let manifest = &async { + let idx = zip.find_entry_index(MANIFEST_FILENAME)?; + zip.read_file(idx).await + } + .await + .map_err(|source| { + error( + LoadModelErrorKind::ReadZipEntry { + filename: MANIFEST_FILENAME.to_owned(), + }, + source, + ) + })?; + let manifest = serde_json::from_slice::(manifest) + .map_err(|source| error(LoadModelErrorKind::InvalidModelFormat, source.into()))?; + + let metas = &async { + let idx = zip.find_entry_index(manifest.metas_filename())?; + zip.read_file(idx).await + } + .await + .map_err(|source| { + error( + LoadModelErrorKind::ReadZipEntry { + filename: manifest.metas_filename().clone(), + }, + source, + ) + })?; + + let header = VoiceModelHeader::new(manifest, metas, path)?; + + InnerTryBuilder { + header, + inference_model_entries_builder: |VoiceModelHeader { manifest, .. }| { + manifest + .domains() + .each_ref() + .map(InferenceDomainMap { + talk: |talk| { + talk.as_ref() + .map(|manifest| { + let indices = enum_map! { + TalkOperation::PredictDuration => { + zip.find_entry_index(&manifest.predict_duration_filename)? + } + TalkOperation::PredictIntonation => zip.find_entry_index( + &manifest.predict_intonation_filename, + )?, + TalkOperation::Decode => { + zip.find_entry_index(&manifest.decode_filename)? + } + }; + + Ok(InferenceModelEntry { indices, manifest }) + }) + .transpose() + .map_err(move |source| { + error( + LoadModelErrorKind::ReadZipEntry { + filename: MANIFEST_FILENAME.to_owned(), + }, + source, + ) + }) + }, + }) + .collect() + .map_err(crate::Error::from) + }, + marker: PhantomData, + } + .try_build() + } + + fn id(&self) -> VoiceModelId { + self.borrow_header().manifest.id + } + + fn metas(&self) -> &VoiceModelMeta { + &self.borrow_header().metas + } + + fn header(&self) -> &VoiceModelHeader { + self.borrow_header() + } + + async fn read_inference_models( + &self, + ) -> LoadModelResult> { + let path = &self.borrow_header().path; + + let error = |context, source| LoadModelError { + path: path.to_owned(), + context, + source: Some(source), + }; + + let mut zip = A::open_zip(path) + .await + .map_err(|source| error(LoadModelErrorKind::OpenZipFile, source))?; + + macro_rules! read_file { + ($entry:expr $(,)?) => {{ + let (index, filename): (usize, Arc) = $entry; + zip.read_file(index) + .map_err(move |source| { + error( + LoadModelErrorKind::ReadZipEntry { + filename: (*filename).to_owned(), + }, + source, + ) + }) + .await? + }}; + } + + let InferenceDomainMap { talk } = + self.with_inference_model_entries(|inference_model_entries| { + inference_model_entries.each_ref().map(InferenceDomainMap { + talk: |talk| { + talk.as_ref() + .map(|InferenceModelEntry { indices, manifest }| { + ( + indices.map(|op, i| (i, manifest[op].clone())), + manifest.style_id_to_inner_voice_id.clone(), + ) + }) + }, + }) + }); + + let talk = OptionFuture::from(talk.map( + |(entries, style_id_to_inner_voice_id)| async move { + let [predict_duration, predict_intonation, decode] = entries.into_array(); + + let predict_duration = read_file!(predict_duration); + let predict_intonation = read_file!(predict_intonation); + let decode = read_file!(decode); + + let model_bytes = + EnumMap::from_array([predict_duration, predict_intonation, decode]); + + Ok((style_id_to_inner_voice_id, model_bytes)) + }, + )) + .await + .transpose()?; + + Ok(InferenceDomainMap { talk }) + } +} + +type InferenceModelEntries<'manifest> = + (Option>,); + +struct InferenceModelEntry { + indices: EnumMap, + manifest: M, +} + +#[ext] +impl A { + async fn open_zip( + path: &Path, + ) -> anyhow::Result> + { + let zip = Self::open_file(path).await.with_context(|| { + // fs-errのと同じにする + format!("failed to open file `{}`", path.display()) + })?; + let zip = futures_util::io::BufReader::new(zip); // async_zip v0.0.16では不要、v0.0.17では必要 + let zip = async_zip::base::read::seek::ZipFileReader::new(zip).await?; + Ok(zip) + } +} + +#[ext] +impl async_zip::base::read::seek::ZipFileReader { + fn find_entry_index(&self, filename: &str) -> anyhow::Result { + let (idx, _) = self + .file() + .entries() + .iter() + .enumerate() + .find(|(_, e)| e.filename().as_str().ok() == Some(filename)) + .with_context(|| "could not find `{filename}`")?; + Ok(idx) + } + + async fn read_file(&mut self, index: usize) -> anyhow::Result> { + let mut rdr = self.reader_with_entry(index).await?; + let mut buf = Vec::with_capacity(rdr.entry().uncompressed_size() as usize); + rdr.read_to_end_checked(&mut buf).await?; + Ok(buf) + } +} + // FIXME: "header"といいつつ、VVMのファイルパスを持っている状態になっている。 /// 音声モデルが持つ、各モデルファイルの実体を除く情報。 /// /// モデルの`[u8]`と分けて`Status`に渡す。 -#[derive(Clone)] pub(crate) struct VoiceModelHeader { pub(crate) manifest: Manifest, /// メタ情報。 @@ -67,27 +303,32 @@ pub(crate) struct VoiceModelHeader { impl VoiceModelHeader { fn new(manifest: Manifest, metas: &[u8], path: &Path) -> LoadModelResult { - let metas = - serde_json::from_slice::(metas).map_err(|source| LoadModelError { - path: path.to_owned(), - context: LoadModelErrorKind::InvalidModelFormat, - source: Some( - anyhow::Error::from(source) - .context(format!("{}が不正です", manifest.metas_filename())), - ), - })?; + let error = |context, source| LoadModelError { + path: path.to_owned(), + context, + source: Some(source), + }; + + let metas = serde_json::from_slice::(metas).map_err(|source| { + error( + LoadModelErrorKind::InvalidModelFormat, + anyhow::Error::from(source) + .context(format!("{}が不正です", manifest.metas_filename())), + ) + })?; manifest .domains() .check_acceptable(&metas) - .map_err(|style_type| LoadModelError { - path: path.to_owned(), - context: LoadModelErrorKind::InvalidModelFormat, - source: Some(anyhow!( - "{metas_filename}には`{style_type}`のスタイルが存在しますが、manifest.jsonでの\ - 対応がありません", - metas_filename = manifest.metas_filename(), - )), + .map_err(|style_type| { + error( + LoadModelErrorKind::InvalidModelFormat, + anyhow!( + "{metas_filename}には`{style_type}`のスタイルが存在しますが、manifest.json\ + での対応がありません", + metas_filename = manifest.metas_filename(), + ), + ) })?; Ok(Self { @@ -98,7 +339,7 @@ impl VoiceModelHeader { } } -impl ManifestDomains { +impl InferenceDomainMap { /// manifestとして対応していない`StyleType`に対してエラーを発する。 /// /// `Status`はこのバリデーションを信頼し、`InferenceDomain`の不足時にパニックする。 @@ -141,360 +382,142 @@ impl ManifestDomains { } pub(crate) mod blocking { - use std::{ - io::{self, Cursor}, - path::Path, - }; + use std::path::Path; use easy_ext::ext; - use enum_map::EnumMap; - use ouroboros::self_referencing; - use rayon::iter::{IntoParallelIterator as _, ParallelIterator as _}; - use serde::de::DeserializeOwned; use uuid::Uuid; use crate::{ - error::{LoadModelError, LoadModelErrorKind, LoadModelResult}, - infer::domains::InferenceDomainMap, - manifest::{Manifest, TalkManifest}, - VoiceModelMeta, + asyncs::SingleTasked, error::LoadModelResult, future::FutureExt as _, + infer::domains::InferenceDomainMap, VoiceModelMeta, }; - use super::{ModelBytesWithInnerVoiceIdsByDomain, VoiceModelHeader, VoiceModelId}; + use super::{Inner, ModelBytesWithInnerVoiceIdsByDomain, VoiceModelHeader, VoiceModelId}; /// 音声モデル。 /// /// VVMファイルと対応する。 - #[derive(Clone)] - pub struct VoiceModel { - header: VoiceModelHeader, - } + pub struct VoiceModel(Inner); impl self::VoiceModel { pub(crate) fn read_inference_models( &self, ) -> LoadModelResult> { - let reader = BlockingVvmEntryReader::open(&self.header.path)?; - - let talk = self - .header - .manifest - .domains() - .talk - .as_ref() - .map( - |TalkManifest { - predict_duration_filename, - predict_intonation_filename, - decode_filename, - style_id_to_inner_voice_id, - }| { - let model_bytes = [ - predict_duration_filename, - predict_intonation_filename, - decode_filename, - ] - .into_par_iter() - .map(|filename| reader.read_vvm_entry(filename)) - .collect::, _>>()? - .try_into() - .unwrap_or_else(|_| panic!("should be same length")); - - let model_bytes = EnumMap::from_array(model_bytes); - - Ok((style_id_to_inner_voice_id.clone(), model_bytes)) - }, - ) - .transpose()?; - - Ok(InferenceDomainMap { talk }) + self.0.read_inference_models().block_on() } /// VVMファイルから`VoiceModel`をコンストラクトする。 pub fn from_path(path: impl AsRef) -> crate::Result { - let path = path.as_ref(); - let reader = BlockingVvmEntryReader::open(path)?; - let manifest = reader.read_vvm_json::("manifest.json")?; - let metas = &reader.read_vvm_entry(manifest.metas_filename())?; - let header = VoiceModelHeader::new(manifest, metas, path)?; - Ok(Self { header }) + Inner::from_path(path).block_on().map(Self) } /// ID。 pub fn id(&self) -> VoiceModelId { - self.header.manifest.id + self.0.id() } /// メタ情報。 pub fn metas(&self) -> &VoiceModelMeta { - &self.header.metas + self.0.metas() } pub(crate) fn header(&self) -> &VoiceModelHeader { - &self.header - } - } - - #[self_referencing] - struct BlockingVvmEntryReader { - path: std::path::PathBuf, - zip: Vec, - #[covariant] - #[borrows(zip)] - reader: zip::ZipArchive>, - } - - impl BlockingVvmEntryReader { - fn open(path: &Path) -> LoadModelResult { - (|| { - let zip = std::fs::read(path)?; - Self::try_new(path.to_owned(), zip, |zip| { - zip::ZipArchive::new(Cursor::new(zip)) - }) - })() - .map_err(|source| LoadModelError { - path: path.to_owned(), - context: LoadModelErrorKind::OpenZipFile, - source: Some(source.into()), - }) - } - - // FIXME: manifest.json専用になっているので、そういう関数名にする - fn read_vvm_json(&self, filename: &str) -> LoadModelResult { - let bytes = &self.read_vvm_entry(filename)?; - serde_json::from_slice(bytes).map_err(|source| LoadModelError { - path: self.borrow_path().clone(), - context: LoadModelErrorKind::InvalidModelFormat, - source: Some(anyhow::Error::from(source).context(format!("{filename}が不正です"))), - }) - } - - fn read_vvm_entry(&self, filename: &str) -> LoadModelResult> { - (|| { - let mut reader = self.borrow_reader().clone(); - let mut entry = reader.by_name(filename)?; - let mut buf = Vec::with_capacity(entry.size() as _); - io::copy(&mut entry, &mut buf)?; - Ok(buf) - })() - .map_err(|source| LoadModelError { - path: self.borrow_path().clone(), - context: LoadModelErrorKind::OpenZipFile, - source: Some(source), - }) + self.0.header() } } #[ext(IdRef)] pub impl VoiceModel { fn id_ref(&self) -> &Uuid { - &self.header.manifest.id.0 + &self.header().manifest.id.0 } } } pub(crate) mod tokio { - use std::{collections::HashMap, io, path::Path}; - - use derive_new::new; - use enum_map::EnumMap; - use futures::future::{join3, OptionFuture}; - use serde::de::DeserializeOwned; + use std::path::Path; use crate::{ - error::{LoadModelError, LoadModelErrorKind, LoadModelResult}, - infer::domains::InferenceDomainMap, - manifest::{Manifest, TalkManifest}, + asyncs::BlockingThreadPool, error::LoadModelResult, infer::domains::InferenceDomainMap, Result, VoiceModelMeta, }; - use super::{ModelBytesWithInnerVoiceIdsByDomain, VoiceModelHeader, VoiceModelId}; + use super::{Inner, ModelBytesWithInnerVoiceIdsByDomain, VoiceModelHeader, VoiceModelId}; /// 音声モデル。 /// /// VVMファイルと対応する。 - #[derive(Clone)] - pub struct VoiceModel { - header: VoiceModelHeader, - } + pub struct VoiceModel(Inner); impl self::VoiceModel { pub(crate) async fn read_inference_models( &self, ) -> LoadModelResult> { - let reader = AsyncVvmEntryReader::open(&self.header.path).await?; - - let talk = OptionFuture::from(self.header.manifest.domains().talk.as_ref().map( - |TalkManifest { - predict_duration_filename, - predict_intonation_filename, - decode_filename, - style_id_to_inner_voice_id, - }| async { - let ( - decode_model_result, - predict_duration_model_result, - predict_intonation_model_result, - ) = join3( - reader.read_vvm_entry(decode_filename), - reader.read_vvm_entry(predict_duration_filename), - reader.read_vvm_entry(predict_intonation_filename), - ) - .await; - - let model_bytes = EnumMap::from_array([ - predict_duration_model_result?, - predict_intonation_model_result?, - decode_model_result?, - ]); - - Ok((style_id_to_inner_voice_id.clone(), model_bytes)) - }, - )) - .await - .transpose()?; - - Ok(InferenceDomainMap { talk }) + self.0.read_inference_models().await } /// VVMファイルから`VoiceModel`をコンストラクトする。 pub async fn from_path(path: impl AsRef) -> Result { - let reader = AsyncVvmEntryReader::open(path.as_ref()).await?; - let manifest = reader.read_vvm_json::("manifest.json").await?; - let metas = &reader.read_vvm_entry(manifest.metas_filename()).await?; - let header = VoiceModelHeader::new(manifest, metas, path.as_ref())?; - Ok(Self { header }) + Inner::from_path(path).await.map(Self) } /// ID。 pub fn id(&self) -> VoiceModelId { - self.header.manifest.id + self.0.id() } /// メタ情報。 pub fn metas(&self) -> &VoiceModelMeta { - &self.header.metas + self.0.metas() } pub(crate) fn header(&self) -> &VoiceModelHeader { - &self.header - } - } - - struct AsyncVvmEntry { - index: usize, - entry: async_zip::ZipEntry, - } - - #[derive(new)] - struct AsyncVvmEntryReader<'a> { - path: &'a Path, - reader: async_zip::base::read::mem::ZipFileReader, - entry_map: HashMap, - } - - impl<'a> AsyncVvmEntryReader<'a> { - async fn open(path: &'a Path) -> LoadModelResult { - let reader = async { - let file = fs_err::tokio::read(path).await?; - async_zip::base::read::mem::ZipFileReader::new(file).await - } - .await - .map_err(|source| LoadModelError { - path: path.to_owned(), - context: LoadModelErrorKind::OpenZipFile, - source: Some(source.into()), - })?; - let entry_map: HashMap<_, _> = reader - .file() - .entries() - .iter() - .flat_map(|e| { - // 非UTF-8のファイルを利用することはないため、無視する - let filename = e.filename().as_str().ok()?; - (!e.dir().ok()?).then_some(())?; - Some((filename.to_owned(), (**e).clone())) - }) - .enumerate() - .map(|(i, (filename, entry))| (filename, AsyncVvmEntry { index: i, entry })) - .collect(); - Ok(AsyncVvmEntryReader::new(path, reader, entry_map)) - } - // FIXME: manifest.json専用になっているので、そういう関数名にする - async fn read_vvm_json(&self, filename: &str) -> LoadModelResult { - let bytes = self.read_vvm_entry(filename).await?; - serde_json::from_slice(&bytes).map_err(|source| LoadModelError { - path: self.path.to_owned(), - context: LoadModelErrorKind::InvalidModelFormat, - source: Some(anyhow::Error::from(source).context(format!("{filename}が不正です"))), - }) - } - - async fn read_vvm_entry(&self, filename: &str) -> LoadModelResult> { - async { - let me = self - .entry_map - .get(filename) - .ok_or_else(|| io::Error::from(io::ErrorKind::NotFound))?; - let mut manifest_reader = self.reader.reader_with_entry(me.index).await?; - let mut buf = Vec::with_capacity(me.entry.uncompressed_size() as usize); - manifest_reader.read_to_end_checked(&mut buf).await?; - Ok::<_, anyhow::Error>(buf) - } - .await - .map_err(|source| LoadModelError { - path: self.path.to_owned(), - context: LoadModelErrorKind::ReadZipEntry { - filename: filename.to_owned(), - }, - source: Some(source), - }) + self.0.header() } } } #[cfg(test)] mod tests { - use std::sync::LazyLock; - use rstest::{fixture, rstest}; use serde_json::json; use crate::{ + infer::domains::InferenceDomainMap, manifest::{ManifestDomains, TalkManifest}, SpeakerMeta, StyleType, }; #[rstest] #[case( - &ManifestDomains { + &InferenceDomainMap { talk: None, }, &[], Ok(()) )] #[case( - &ManifestDomains { - talk: Some(TALK_MANIFEST.clone()), + &InferenceDomainMap { + talk: Some(TalkManifest::default()), }, &[speaker(&[StyleType::Talk])], Ok(()) )] #[case( - &ManifestDomains { - talk: Some(TALK_MANIFEST.clone()), + &InferenceDomainMap { + talk: Some(TalkManifest::default()), }, &[speaker(&[StyleType::Talk, StyleType::Sing])], Ok(()) )] #[case( - &ManifestDomains { + &InferenceDomainMap { talk: None, }, &[speaker(&[StyleType::Talk])], Err(()) )] fn check_acceptable_works( - #[case] manifest: &ManifestDomains, + #[case] manifest: &InferenceDomainMap, #[case] metas: &[SpeakerMeta], #[case] expected: std::result::Result<(), ()>, ) { @@ -502,13 +525,7 @@ mod tests { assert_eq!(expected, actual); } - static TALK_MANIFEST: LazyLock = LazyLock::new(|| TalkManifest { - predict_duration_filename: "".to_owned(), - predict_intonation_filename: "".to_owned(), - decode_filename: "".to_owned(), - style_id_to_inner_voice_id: Default::default(), - }); - + // FIXME: これ使ってないのでは? #[fixture] fn talk_speaker() -> SpeakerMeta { serde_json::from_value(json!({ diff --git a/crates/voicevox_core_c_api/Cargo.toml b/crates/voicevox_core_c_api/Cargo.toml index 996367a5f..1b74bfdf5 100644 --- a/crates/voicevox_core_c_api/Cargo.toml +++ b/crates/voicevox_core_c_api/Cargo.toml @@ -26,7 +26,6 @@ const_format.workspace = true cstr.workspace = true derive-getters.workspace = true easy-ext.workspace = true -futures.workspace = true itertools.workspace = true libc.workspace = true process_path.workspace = true diff --git a/crates/voicevox_core_c_api/src/compatible_engine.rs b/crates/voicevox_core_c_api/src/compatible_engine.rs index 68b836f2f..9fdff0c92 100644 --- a/crates/voicevox_core_c_api/src/compatible_engine.rs +++ b/crates/voicevox_core_c_api/src/compatible_engine.rs @@ -2,7 +2,7 @@ use std::{ collections::BTreeMap, env, ffi::{c_char, CString}, - sync::{LazyLock, Mutex, MutexGuard}, + sync::{Arc, LazyLock, Mutex, MutexGuard}, }; use libc::c_int; @@ -35,10 +35,10 @@ static ONNXRUNTIME: LazyLock<&'static voicevox_core::blocking::Onnxruntime> = La }); struct VoiceModelSet { - all_vvms: Vec, + all_vvms: Vec>, all_metas_json: CString, style_model_map: BTreeMap, - model_map: BTreeMap, + model_map: BTreeMap>, } static VOICE_MODEL_SET: LazyLock = LazyLock::new(|| { @@ -66,7 +66,7 @@ static VOICE_MODEL_SET: LazyLock = LazyLock::new(|| { /// # Panics /// /// 失敗したらパニックする - fn get_all_models() -> Vec { + fn get_all_models() -> Vec> { let root_dir = if let Some(root_dir) = env::var_os(ROOT_DIR_ENV_NAME) { root_dir.into() } else { @@ -84,7 +84,7 @@ static VOICE_MODEL_SET: LazyLock = LazyLock::new(|| { .unwrap_or_else(|e| panic!("{}が読めませんでした: {e}", root_dir.display())) .into_iter() .filter(|entry| entry.path().extension().map_or(false, |ext| ext == "vvm")) - .map(|entry| voicevox_core::blocking::VoiceModel::from_path(entry.path())) + .map(|entry| voicevox_core::blocking::VoiceModel::from_path(entry.path()).map(Arc::new)) .collect::>() .unwrap() } diff --git a/crates/voicevox_core_macros/src/extract.rs b/crates/voicevox_core_macros/src/extract.rs new file mode 100644 index 000000000..e9b480630 --- /dev/null +++ b/crates/voicevox_core_macros/src/extract.rs @@ -0,0 +1,31 @@ +use syn::{ + spanned::Spanned as _, Attribute, Data, DataEnum, DataStruct, DataUnion, Field, Fields, Type, +}; + +pub(crate) fn struct_fields(data: &Data) -> syn::Result> { + let fields = match data { + Data::Struct(DataStruct { + fields: Fields::Named(fields), + .. + }) => fields, + Data::Struct(DataStruct { fields, .. }) => { + return Err(syn::Error::new(fields.span(), "expect named fields")); + } + Data::Enum(DataEnum { enum_token, .. }) => { + return Err(syn::Error::new(enum_token.span(), "expected a struct")); + } + Data::Union(DataUnion { union_token, .. }) => { + return Err(syn::Error::new(union_token.span(), "expected a struct")); + } + }; + + Ok(fields + .named + .iter() + .map( + |Field { + attrs, ident, ty, .. + }| (&**attrs, ident.as_ref().expect("should be named"), ty), + ) + .collect()) +} diff --git a/crates/voicevox_core_macros/src/inference_domain.rs b/crates/voicevox_core_macros/src/inference_domain.rs index d24a20ab1..f959982e4 100644 --- a/crates/voicevox_core_macros/src/inference_domain.rs +++ b/crates/voicevox_core_macros/src/inference_domain.rs @@ -3,8 +3,8 @@ use quote::quote; use syn::{ parse::{Parse, ParseStream}, spanned::Spanned as _, - Attribute, Data, DataEnum, DataStruct, DataUnion, DeriveInput, Field, Fields, Generics, - ItemType, Type, Variant, + Attribute, Data, DataEnum, DataStruct, DataUnion, DeriveInput, Fields, Generics, ItemType, + Type, Variant, }; pub(crate) fn derive_inference_operation( @@ -178,11 +178,11 @@ pub(crate) fn derive_inference_input_signature( let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let fields = struct_fields(data)?; + let fields = crate::extract::struct_fields(data)?; let param_infos = fields .iter() - .map(|(name, ty)| { + .map(|(_, name, ty)| { let name = name.to_string(); quote! { crate::infer::ParamInfo { @@ -194,7 +194,7 @@ pub(crate) fn derive_inference_input_signature( }) .collect::(); - let field_names = fields.iter().map(|(name, _)| name); + let field_names = fields.iter().map(|(_, name, _)| name); return Ok(quote! { impl #impl_generics crate::infer::InferenceInputSignature for #ident #ty_generics @@ -277,12 +277,12 @@ pub(crate) fn derive_inference_output_signature( let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let fields = struct_fields(data)?; + let fields = crate::extract::struct_fields(data)?; let num_fields = fields.len(); let param_infos = fields .iter() - .map(|(name, ty)| { + .map(|(_, name, ty)| { let name = name.to_string(); quote! { crate::infer::ParamInfo { @@ -294,7 +294,7 @@ pub(crate) fn derive_inference_output_signature( }) .collect::(); - let field_names = fields.iter().map(|(name, _)| name); + let field_names = fields.iter().map(|(_, name, _)| name); Ok(quote! { impl #impl_generics crate::infer::InferenceOutputSignature for #ident #ty_generics @@ -349,30 +349,6 @@ pub(crate) fn derive_inference_output_signature( }) } -fn struct_fields(data: &Data) -> syn::Result> { - let fields = match data { - Data::Struct(DataStruct { - fields: Fields::Named(fields), - .. - }) => fields, - Data::Struct(DataStruct { fields, .. }) => { - return Err(syn::Error::new(fields.span(), "expect named fields")); - } - Data::Enum(DataEnum { enum_token, .. }) => { - return Err(syn::Error::new(enum_token.span(), "expected a struct")); - } - Data::Union(DataUnion { union_token, .. }) => { - return Err(syn::Error::new(union_token.span(), "expected a struct")); - } - }; - - Ok(fields - .named - .iter() - .map(|Field { ident, ty, .. }| (ident.as_ref().expect("should be named"), ty)) - .collect()) -} - fn unit_enum_variants(data: &Data) -> syn::Result> { let variants = match data { Data::Struct(DataStruct { struct_token, .. }) => { diff --git a/crates/voicevox_core_macros/src/lib.rs b/crates/voicevox_core_macros/src/lib.rs index 98a2fdc5c..ff0b83037 100644 --- a/crates/voicevox_core_macros/src/lib.rs +++ b/crates/voicevox_core_macros/src/lib.rs @@ -1,6 +1,8 @@ #![warn(rust_2018_idioms)] +mod extract; mod inference_domain; +mod manifest; use syn::parse_macro_input; @@ -100,6 +102,35 @@ pub fn derive_inference_output_signature( from_syn(inference_domain::derive_inference_output_signature(input)) } +/// 構造体のフィールドを取得できる`std::ops::Index`の実装を導出する。 +/// +/// # Example +/// +/// ``` +/// use macros::IndexForFields; +/// +/// #[derive(IndexForFields)] +/// #[index_for_fields(TalkOperation)] +/// pub(crate) struct TalkManifest { +/// #[index_for_fields(TalkOperation::PredictDuration)] +/// pub(crate) predict_duration_filename: Arc, +/// +/// #[index_for_fields(TalkOperation::PredictIntonation)] +/// pub(crate) predict_intonation_filename: Arc, +/// +/// #[index_for_fields(TalkOperation::Decode)] +/// pub(crate) decode_filename: Arc, +/// +/// // … +/// } +/// ``` +#[cfg(not(doctest))] +#[proc_macro_derive(IndexForFields, attributes(index_for_fields))] +pub fn derive_index_for_fields(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = &parse_macro_input!(input); + from_syn(manifest::derive_index_for_fields(input)) +} + fn from_syn(result: syn::Result) -> proc_macro::TokenStream { result.unwrap_or_else(|e| e.to_compile_error()).into() } diff --git a/crates/voicevox_core_macros/src/manifest.rs b/crates/voicevox_core_macros/src/manifest.rs new file mode 100644 index 000000000..9560b1fd4 --- /dev/null +++ b/crates/voicevox_core_macros/src/manifest.rs @@ -0,0 +1,72 @@ +use proc_macro2::Span; +use quote::quote; +use syn::{Attribute, DeriveInput, Expr, Meta, Type}; + +pub(crate) fn derive_index_for_fields( + input: &DeriveInput, +) -> syn::Result { + const ATTR_NAME: &str = "index_for_fields"; + + let DeriveInput { + attrs, + ident, + generics, + data, + .. + } = input; + + let idx = attrs + .iter() + .find_map(|Attribute { meta, .. }| match meta { + Meta::List(list) if list.path.is_ident(ATTR_NAME) => Some(list), + _ => None, + }) + .ok_or_else(|| { + syn::Error::new( + Span::call_site(), + format!("missing `#[{ATTR_NAME}(…)]` in the struct itself"), + ) + })? + .parse_args::()?; + + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let targets = crate::extract::struct_fields(data)? + .into_iter() + .flat_map(|(attrs, name, output)| { + let meta = attrs.iter().find_map(|Attribute { meta, .. }| match meta { + Meta::List(meta) if meta.path.is_ident(ATTR_NAME) => Some(meta), + _ => None, + })?; + Some((meta, name, output)) + }) + .map(|(meta, name, output)| { + let key = meta.parse_args::()?; + Ok((key, name, output)) + }) + .collect::>>()?; + + let (_, _, output) = targets.first().ok_or_else(|| { + syn::Error::new( + Span::call_site(), + format!("no fields have `#[{ATTR_NAME}(…)]`"), + ) + })?; + + let arms = targets + .iter() + .map(|(key, name, _)| Ok(quote!(#key => &self.#name))) + .collect::>>()?; + + Ok(quote! { + impl #impl_generics ::std::ops::Index<#idx> for #ident #ty_generics #where_clause { + type Output = #output; + + fn index(&self, index: #idx) -> &Self::Output { + match index { + #(#arms),* + } + } + } + }) +} diff --git a/crates/voicevox_core_python_api/src/lib.rs b/crates/voicevox_core_python_api/src/lib.rs index 9eabae6a3..b4aa65c9b 100644 --- a/crates/voicevox_core_python_api/src/lib.rs +++ b/crates/voicevox_core_python_api/src/lib.rs @@ -160,14 +160,16 @@ mod blocking { #[pyclass] #[derive(Clone)] pub(crate) struct VoiceModel { - model: voicevox_core::blocking::VoiceModel, + model: Arc, } #[pymethods] impl VoiceModel { #[staticmethod] fn from_path(py: Python<'_>, path: PathBuf) -> PyResult { - let model = voicevox_core::blocking::VoiceModel::from_path(path).into_py_result(py)?; + let model = voicevox_core::blocking::VoiceModel::from_path(path) + .into_py_result(py)? + .into(); Ok(Self { model }) } @@ -660,7 +662,7 @@ mod asyncio { #[pyclass] #[derive(Clone)] pub(crate) struct VoiceModel { - model: voicevox_core::tokio::VoiceModel, + model: Arc, } #[pymethods] @@ -669,7 +671,7 @@ mod asyncio { fn from_path(py: Python<'_>, path: PathBuf) -> PyResult<&PyAny> { pyo3_asyncio::tokio::future_into_py(py, async move { let model = voicevox_core::tokio::VoiceModel::from_path(path).await; - let model = Python::with_gil(|py| model.into_py_result(py))?; + let model = Python::with_gil(|py| model.into_py_result(py))?.into(); Ok(Self { model }) }) }