From d66ebcf82c5c548de60ced217a2de1408a44df96 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Sun, 8 Sep 2024 05:35:18 +0900 Subject: [PATCH 01/18] improve: rework `VoiceModel` --- Cargo.lock | 112 ++++- Cargo.toml | 3 +- crates/voicevox_core/Cargo.toml | 7 +- crates/voicevox_core/src/infer/domains.rs | 52 ++ crates/voicevox_core/src/manifest.rs | 30 +- crates/voicevox_core/src/voice_model.rs | 446 +++++++++--------- crates/voicevox_core_c_api/Cargo.toml | 1 - .../src/compatible_engine.rs | 10 +- crates/voicevox_core_macros/src/extract.rs | 31 ++ .../src/inference_domain.rs | 40 +- crates/voicevox_core_macros/src/lib.rs | 9 + crates/voicevox_core_macros/src/manifest.rs | 60 +++ crates/voicevox_core_python_api/src/lib.rs | 10 +- 13 files changed, 514 insertions(+), 297 deletions(-) create mode 100644 crates/voicevox_core_macros/src/extract.rs create mode 100644 crates/voicevox_core_macros/src/manifest.rs 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/infer/domains.rs b/crates/voicevox_core/src/infer/domains.rs index 687550399..58009663f 100644 --- a/crates/voicevox_core/src/infer/domains.rs +++ b/crates/voicevox_core/src/infer/domains.rs @@ -1,14 +1,66 @@ mod talk; +use std::future::Future; + +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 { + pub(crate) fn ref_map<'a, T, Ft: FnOnce(&'a V::Talk) -> T>( + &'a self, + fs: InferenceDomainMap<(Ft,)>, + ) -> InferenceDomainMap<(T,)> { + InferenceDomainMap { + talk: (fs.talk)(&self.talk), + } + } +} + +impl InferenceDomainMap<(Result,)> { + pub(crate) fn collect_results(self) -> Result, E> { + let talk = self.talk?; + Ok(InferenceDomainMap { talk }) + } +} + +impl>, T, E> InferenceDomainMap<(Ft,)> { + pub(crate) async fn collect_future_results(self) -> Result, E> { + let talk = self.talk.await?; + 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/manifest.rs b/crates/voicevox_core/src/manifest.rs index 4460f10bf..70db33d2a 100644 --- a/crates/voicevox_core/src/manifest.rs +++ b/crates/voicevox_core/src/manifest.rs @@ -10,7 +10,10 @@ use derive_new::new; 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 +68,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, macros::Index)] +#[cfg_attr(test, derive(Default))] +#[index(TalkOperation)] pub(crate) struct TalkManifest { - pub(crate) predict_duration_filename: String, - pub(crate) predict_intonation_filename: String, - pub(crate) decode_filename: String, + #[index(TalkOperation::PredictDuration)] + pub(crate) predict_duration_filename: Arc, + + #[index(TalkOperation::PredictIntonation)] + pub(crate) predict_intonation_filename: Arc, + + #[index(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..7ecf59caf 100644 --- a/crates/voicevox_core/src/voice_model.rs +++ b/crates/voicevox_core/src/voice_model.rs @@ -13,7 +13,7 @@ use uuid::Uuid; use crate::{ error::{LoadModelError, LoadModelErrorKind, LoadModelResult}, infer::{ - domains::{TalkDomain, TalkOperation}, + domains::{InferenceDomainMap, TalkDomain, TalkOperation}, InferenceDomain, }, manifest::{Manifest, ManifestDomains, StyleIdToInnerVoiceId}, @@ -55,7 +55,6 @@ impl VoiceModelId { /// 音声モデルが持つ、各モデルファイルの実体を除く情報。 /// /// モデルの`[u8]`と分けて`Status`に渡す。 -#[derive(Clone)] pub(crate) struct VoiceModelHeader { pub(crate) manifest: Manifest, /// メタ情報。 @@ -98,7 +97,7 @@ impl VoiceModelHeader { } } -impl ManifestDomains { +impl InferenceDomainMap { /// manifestとして対応していない`StyleType`に対してエラーを発する。 /// /// `Status`はこのバリデーションを信頼し、`InferenceDomain`の不足時にパニックする。 @@ -141,169 +140,70 @@ 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, - }; + use crate::{error::LoadModelResult, infer::domains::InferenceDomainMap, VoiceModelMeta}; use super::{ModelBytesWithInnerVoiceIdsByDomain, VoiceModelHeader, VoiceModelId}; /// 音声モデル。 /// /// VVMファイルと対応する。 - #[derive(Clone)] - pub struct VoiceModel { - header: VoiceModelHeader, - } + pub struct VoiceModel(super::tokio::VoiceModel); 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 }) + futures_lite::future::block_on(self.0.read_inference_models()) } /// 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 }) + futures_lite::future::block_on(super::tokio::VoiceModel::from_path(path)).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 std::{path::Path, sync::Arc}; - use derive_new::new; - use enum_map::EnumMap; - use futures::future::{join3, OptionFuture}; - use serde::de::DeserializeOwned; + use anyhow::Context as _; + use easy_ext::ext; + use enum_map::{enum_map, EnumMap}; + use futures_util::{future::OptionFuture, TryFutureExt as _}; + use ouroboros::self_referencing; use crate::{ error::{LoadModelError, LoadModelErrorKind, LoadModelResult}, - infer::domains::InferenceDomainMap, + infer::{ + domains::{InferenceDomainMap, TalkDomain, TalkOperation}, + InferenceDomain, + }, manifest::{Manifest, TalkManifest}, Result, VoiceModelMeta, }; @@ -313,188 +213,272 @@ pub(crate) mod tokio { /// 音声モデル。 /// /// VVMファイルと対応する。 - #[derive(Clone)] + #[self_referencing] pub struct VoiceModel { header: VoiceModelHeader, + + #[borrows(header)] + #[not_covariant] + inference_model_entries: InferenceDomainMap>, } 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 path = &self.borrow_header().path; - let model_bytes = EnumMap::from_array([ - predict_duration_model_result?, - predict_intonation_model_result?, - decode_model_result?, - ]); + let error = |context, source| LoadModelError { + path: path.to_owned(), + context, + source: Some(source), + }; + + let mut zip = async_zip::base::read::seek::ZipFileReader::from_file(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? + }}; + } - Ok((style_id_to_inner_voice_id.clone(), model_bytes)) - }, - )) + self.with_inference_model_entries(|inference_model_entries| { + inference_model_entries + .ref_map(InferenceDomainMap { + talk: |talk| { + let talk = + talk.as_ref() + .map(|InferenceModelEntry { indices, manifest }| { + ( + indices.map(|op, i| (i, manifest[op].clone())), + manifest.style_id_to_inner_voice_id.clone(), + ) + }); + async { + OptionFuture::from(talk.map( + |(entries, style_id_to_inner_voice_id)| async { + 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() + } + }, + }) + .collect_future_results() + }) .await - .transpose()?; - - Ok(InferenceDomainMap { talk }) } /// 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 }) + 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 = async_zip::base::read::seek::ZipFileReader::from_file(path) + .await + .map_err(|source| error(LoadModelErrorKind::OpenZipFile, source))?; + + let manifest = &async { zip.read_file(zip.find_index(MANIFEST_FILENAME)?).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 { + zip.read_file(zip.find_index(manifest.metas_filename())?) + .await + } + .await + .map_err(|source| { + error( + LoadModelErrorKind::ReadZipEntry { + filename: manifest.metas_filename().clone(), + }, + source, + ) + })?; + + let header = VoiceModelHeader::new(manifest, metas, path)?; + + VoiceModelTryBuilder { + header, + inference_model_entries_builder: |VoiceModelHeader { manifest, .. }| { + manifest + .domains() + .ref_map(InferenceDomainMap { + talk: |talk| { + talk.as_ref() + .map(|manifest| { + let indices = enum_map! { + TalkOperation::PredictDuration => { + zip.find_index(&manifest.predict_duration_filename)? + } + TalkOperation::PredictIntonation => zip.find_index( + &manifest.predict_intonation_filename, + )?, + TalkOperation::Decode => { + zip.find_index(&manifest.decode_filename)? + } + }; + + Ok(InferenceModelEntry { indices, manifest }) + }) + .transpose() + .map_err(move |source| LoadModelError { + path: path.to_owned(), + context: LoadModelErrorKind::ReadZipEntry { + filename: MANIFEST_FILENAME.to_owned(), + }, + source: Some(source), + }) + }, + }) + .collect_results() + .map_err(crate::Error::from) + }, + } + .try_build() } /// ID。 pub fn id(&self) -> VoiceModelId { - self.header.manifest.id + self.borrow_header().manifest.id } /// メタ情報。 pub fn metas(&self) -> &VoiceModelMeta { - &self.header.metas + &self.borrow_header().metas } pub(crate) fn header(&self) -> &VoiceModelHeader { - &self.header + self.borrow_header() } } - struct AsyncVvmEntry { - index: usize, - entry: async_zip::ZipEntry, - } + type InferenceModelEntries<'manifest> = + (Option>,); - #[derive(new)] - struct AsyncVvmEntryReader<'a> { - path: &'a Path, - reader: async_zip::base::read::mem::ZipFileReader, - entry_map: HashMap, + struct InferenceModelEntry { + indices: EnumMap, + manifest: M, } - 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()), + #[ext] + impl async_zip::base::read::seek::ZipFileReader> { + async fn from_file(path: &Path) -> anyhow::Result + where + Self: Sized, + { + let zip = async_fs::File::open(path).await.with_context(|| { + // fs-errのと同じにする + format!("failed to open file `{}`", path.display()) })?; - let entry_map: HashMap<_, _> = reader + let zip = futures_util::io::BufReader::new(zip); + let zip = async_zip::base::read::seek::ZipFileReader::new(zip).await?; + Ok(zip) + } + + fn find_index(&self, filename: &str) -> anyhow::Result { + let (idx, _) = self .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}が不正です"))), - }) + .find(|(_, e)| e.filename().as_str().ok() == Some(filename)) + .with_context(|| "could not find `{filename}`")?; + Ok(idx) } - 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), - }) + 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) } } } #[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 +486,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..a6ced51dd --- /dev/null +++ b/crates/voicevox_core_macros/src/extract.rs @@ -0,0 +1,31 @@ +use syn::{spanned::Spanned as _, 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..601481de7 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,13 @@ pub fn derive_inference_output_signature( from_syn(inference_domain::derive_inference_output_signature(input)) } +#[cfg(not(doctest))] +#[proc_macro_derive(Index, attributes(index))] +pub fn derive_index(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = &parse_macro_input!(input); + from_syn(manifest::derive_index(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..f4d14ba78 --- /dev/null +++ b/crates/voicevox_core_macros/src/manifest.rs @@ -0,0 +1,60 @@ +use proc_macro2::Span; +use quote::quote; +use syn::{Attribute, DeriveInput, Expr, Meta, Type}; + +pub(crate) fn derive_index(input: &DeriveInput) -> syn::Result { + 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("index") => Some(list), + _ => None, + }) + .ok_or_else(|| syn::Error::new(Span::call_site(), "missing `#[index(…)]`"))? + .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, ty)| { + let list = attrs.iter().find_map(|Attribute { meta, .. }| match meta { + Meta::List(list) if list.path.is_ident("index") => Some(list), + _ => None, + })?; + Some((list, name, ty)) + }) + .map(|(list, name, ty)| { + let key = list.parse_args::()?; + Ok((key, name, ty)) + }) + .collect::>>()?; + + let (_, _, output) = targets + .first() + .ok_or_else(|| syn::Error::new(Span::call_site(), "no fields with `#[index(…)]`"))?; + + 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 }) }) } From 875963487d8f9d3bc5a030ba27666d5ff193bc67 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Sun, 8 Sep 2024 17:08:42 +0900 Subject: [PATCH 02/18] =?UTF-8?q?diff=E3=82=92=E6=8A=91=E3=81=88=E3=82=8B?= =?UTF-8?q?=E5=B7=A5=E5=A4=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/voicevox_core/src/voice_model.rs | 125 ++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/crates/voicevox_core/src/voice_model.rs b/crates/voicevox_core/src/voice_model.rs index 7ecf59caf..d73813d1c 100644 --- a/crates/voicevox_core/src/voice_model.rs +++ b/crates/voicevox_core/src/voice_model.rs @@ -181,6 +181,57 @@ pub(crate) mod blocking { } } + // FIXME: Gitのdiffを抑えるためだけにコメントアウト状態で残してある状態 + //#[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), + // }) + // } + //} + #[ext(IdRef)] pub impl VoiceModel { fn id_ref(&self) -> &Uuid { @@ -394,6 +445,80 @@ pub(crate) mod tokio { } } + #[cfg(any())] // FIXME: Gitのdiffを抑えるためだけに残してある状態 + struct AsyncVvmEntry { + index: usize, + entry: async_zip::ZipEntry, + } + + #[cfg(any())] // FIXME: Gitのdiffを抑えるためだけに残してある状態 + #[derive(new)] + struct AsyncVvmEntryReader<'a> { + path: &'a Path, + reader: async_zip::base::read::mem::ZipFileReader, + entry_map: HashMap, + } + + #[cfg(any())] // FIXME: Gitのdiffを抑えるためだけに残してある状態 + 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), + }) + } + } + type InferenceModelEntries<'manifest> = (Option>,); From e2ec9b8869e3e247c0cb3a4206c1e5c57e392f33 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Sun, 8 Sep 2024 17:20:21 +0900 Subject: [PATCH 03/18] Minor refactor --- crates/voicevox_core/src/manifest.rs | 11 ++++---- crates/voicevox_core/src/voice_model.rs | 2 +- crates/voicevox_core_macros/src/lib.rs | 28 ++++++++++++++++++--- crates/voicevox_core_macros/src/manifest.rs | 21 ++++++++++------ 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/crates/voicevox_core/src/manifest.rs b/crates/voicevox_core/src/manifest.rs index 70db33d2a..203fc76a9 100644 --- a/crates/voicevox_core/src/manifest.rs +++ b/crates/voicevox_core/src/manifest.rs @@ -7,6 +7,7 @@ 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}; @@ -80,17 +81,17 @@ pub struct Manifest { pub(crate) type ManifestDomains = (Option,); -#[derive(Deserialize, macros::Index)] +#[derive(Deserialize, IndexForFields)] #[cfg_attr(test, derive(Default))] -#[index(TalkOperation)] +#[index_for_fields(TalkOperation)] pub(crate) struct TalkManifest { - #[index(TalkOperation::PredictDuration)] + #[index_for_fields(TalkOperation::PredictDuration)] pub(crate) predict_duration_filename: Arc, - #[index(TalkOperation::PredictIntonation)] + #[index_for_fields(TalkOperation::PredictIntonation)] pub(crate) predict_intonation_filename: Arc, - #[index(TalkOperation::Decode)] + #[index_for_fields(TalkOperation::Decode)] pub(crate) decode_filename: Arc, #[serde(default)] diff --git a/crates/voicevox_core/src/voice_model.rs b/crates/voicevox_core/src/voice_model.rs index d73813d1c..ac47b378c 100644 --- a/crates/voicevox_core/src/voice_model.rs +++ b/crates/voicevox_core/src/voice_model.rs @@ -531,7 +531,7 @@ pub(crate) mod tokio { impl async_zip::base::read::seek::ZipFileReader> { async fn from_file(path: &Path) -> anyhow::Result where - Self: Sized, + Self: Sized, // 自明 { let zip = async_fs::File::open(path).await.with_context(|| { // fs-errのと同じにする diff --git a/crates/voicevox_core_macros/src/lib.rs b/crates/voicevox_core_macros/src/lib.rs index 601481de7..866a9531b 100644 --- a/crates/voicevox_core_macros/src/lib.rs +++ b/crates/voicevox_core_macros/src/lib.rs @@ -102,11 +102,33 @@ 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(Index, attributes(index))] -pub fn derive_index(input: proc_macro::TokenStream) -> proc_macro::TokenStream { +#[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(input)) + from_syn(manifest::derive_index_for_fields(input)) } fn from_syn(result: syn::Result) -> proc_macro::TokenStream { diff --git a/crates/voicevox_core_macros/src/manifest.rs b/crates/voicevox_core_macros/src/manifest.rs index f4d14ba78..e66281130 100644 --- a/crates/voicevox_core_macros/src/manifest.rs +++ b/crates/voicevox_core_macros/src/manifest.rs @@ -2,7 +2,9 @@ use proc_macro2::Span; use quote::quote; use syn::{Attribute, DeriveInput, Expr, Meta, Type}; -pub(crate) fn derive_index(input: &DeriveInput) -> syn::Result { +pub(crate) fn derive_index_for_fields( + input: &DeriveInput, +) -> syn::Result { let DeriveInput { attrs, ident, @@ -14,10 +16,15 @@ pub(crate) fn derive_index(input: &DeriveInput) -> syn::Result Some(list), + Meta::List(list) if list.path.is_ident("index_for_fields") => Some(list), _ => None, }) - .ok_or_else(|| syn::Error::new(Span::call_site(), "missing `#[index(…)]`"))? + .ok_or_else(|| { + syn::Error::new( + Span::call_site(), + "missing `#[index_for_fields(…)]` in the struct itself", + ) + })? .parse_args::()?; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); @@ -26,7 +33,7 @@ pub(crate) fn derive_index(input: &DeriveInput) -> syn::Result Some(list), + Meta::List(list) if list.path.is_ident("index_for_fields") => Some(list), _ => None, })?; Some((list, name, ty)) @@ -37,9 +44,9 @@ pub(crate) fn derive_index(input: &DeriveInput) -> syn::Result>>()?; - let (_, _, output) = targets - .first() - .ok_or_else(|| syn::Error::new(Span::call_site(), "no fields with `#[index(…)]`"))?; + let (_, _, output) = targets.first().ok_or_else(|| { + syn::Error::new(Span::call_site(), "no fields have `#[index_for_fields(…)]`") + })?; let arms = targets .iter() From 45756931a6f4e58fdd89cb09cd7d191ed4adf01e Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Sun, 8 Sep 2024 19:50:11 +0900 Subject: [PATCH 04/18] =?UTF-8?q?`.ref=5Fmap(=E2=80=A6)`=20=E2=86=92=20`.e?= =?UTF-8?q?ach=5Fref().map(=E2=80=A6)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/voicevox_core/src/infer/domains.rs | 14 +++++++++----- crates/voicevox_core/src/voice_model.rs | 6 ++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/voicevox_core/src/infer/domains.rs b/crates/voicevox_core/src/infer/domains.rs index 58009663f..3fcaae473 100644 --- a/crates/voicevox_core/src/infer/domains.rs +++ b/crates/voicevox_core/src/infer/domains.rs @@ -18,13 +18,17 @@ pub(crate) struct InferenceDomainMap { pub(crate) talk: V::Talk, } -impl InferenceDomainMap { - pub(crate) fn ref_map<'a, T, Ft: FnOnce(&'a V::Talk) -> T>( - &'a self, +impl InferenceDomainMap<(T,)> { + pub(crate) fn each_ref(&self) -> InferenceDomainMap<(&T,)> { + InferenceDomainMap { talk: &self.talk } + } + + pub(crate) fn map T2>( + self, fs: InferenceDomainMap<(Ft,)>, - ) -> InferenceDomainMap<(T,)> { + ) -> InferenceDomainMap<(T2,)> { InferenceDomainMap { - talk: (fs.talk)(&self.talk), + talk: (fs.talk)(self.talk), } } } diff --git a/crates/voicevox_core/src/voice_model.rs b/crates/voicevox_core/src/voice_model.rs index ac47b378c..c24ff2e45 100644 --- a/crates/voicevox_core/src/voice_model.rs +++ b/crates/voicevox_core/src/voice_model.rs @@ -307,7 +307,8 @@ pub(crate) mod tokio { self.with_inference_model_entries(|inference_model_entries| { inference_model_entries - .ref_map(InferenceDomainMap { + .each_ref() + .map(InferenceDomainMap { talk: |talk| { let talk = talk.as_ref() @@ -395,7 +396,8 @@ pub(crate) mod tokio { inference_model_entries_builder: |VoiceModelHeader { manifest, .. }| { manifest .domains() - .ref_map(InferenceDomainMap { + .each_ref() + .map(InferenceDomainMap { talk: |talk| { talk.as_ref() .map(|manifest| { From 05fe9d1944cb50fd618a51e1682e1e5e744db83f Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Sun, 8 Sep 2024 20:17:07 +0900 Subject: [PATCH 05/18] =?UTF-8?q?`collect=5Fresults`=20=E2=86=92=20`collec?= =?UTF-8?q?t`,=20`collect=5Ffuture=5Fresults`=20=E2=86=92=20`join`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/voicevox_core/src/infer/domains.rs | 4 ++-- crates/voicevox_core/src/voice_model.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/voicevox_core/src/infer/domains.rs b/crates/voicevox_core/src/infer/domains.rs index 3fcaae473..b90b347ef 100644 --- a/crates/voicevox_core/src/infer/domains.rs +++ b/crates/voicevox_core/src/infer/domains.rs @@ -34,14 +34,14 @@ impl InferenceDomainMap<(T,)> { } impl InferenceDomainMap<(Result,)> { - pub(crate) fn collect_results(self) -> Result, E> { + pub(crate) fn collect(self) -> Result, E> { let talk = self.talk?; Ok(InferenceDomainMap { talk }) } } impl>, T, E> InferenceDomainMap<(Ft,)> { - pub(crate) async fn collect_future_results(self) -> Result, E> { + pub(crate) async fn join(self) -> Result, E> { let talk = self.talk.await?; Ok(InferenceDomainMap { talk }) } diff --git a/crates/voicevox_core/src/voice_model.rs b/crates/voicevox_core/src/voice_model.rs index c24ff2e45..8d09a2b29 100644 --- a/crates/voicevox_core/src/voice_model.rs +++ b/crates/voicevox_core/src/voice_model.rs @@ -342,7 +342,7 @@ pub(crate) mod tokio { } }, }) - .collect_future_results() + .join() }) .await } @@ -425,7 +425,7 @@ pub(crate) mod tokio { }) }, }) - .collect_results() + .collect() .map_err(crate::Error::from) }, } From 38ff58712b38b886de33a3729e527e53500ee6df Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Sun, 8 Sep 2024 20:24:54 +0900 Subject: [PATCH 06/18] =?UTF-8?q?`join`=E3=81=8B=E3=82=89`Result`=E3=81=AE?= =?UTF-8?q?`collect`=E3=82=92=E5=88=86=E9=9B=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/voicevox_core/src/infer/domains.rs | 8 ++++---- crates/voicevox_core/src/voice_model.rs | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/voicevox_core/src/infer/domains.rs b/crates/voicevox_core/src/infer/domains.rs index b90b347ef..e9246af70 100644 --- a/crates/voicevox_core/src/infer/domains.rs +++ b/crates/voicevox_core/src/infer/domains.rs @@ -40,10 +40,10 @@ impl InferenceDomainMap<(Result,)> { } } -impl>, T, E> InferenceDomainMap<(Ft,)> { - pub(crate) async fn join(self) -> Result, E> { - let talk = self.talk.await?; - Ok(InferenceDomainMap { talk }) +impl InferenceDomainMap<(T,)> { + pub(crate) async fn join(self) -> InferenceDomainMap<(T::Output,)> { + let talk = self.talk.await; + InferenceDomainMap { talk } } } diff --git a/crates/voicevox_core/src/voice_model.rs b/crates/voicevox_core/src/voice_model.rs index 8d09a2b29..6064d847d 100644 --- a/crates/voicevox_core/src/voice_model.rs +++ b/crates/voicevox_core/src/voice_model.rs @@ -246,7 +246,7 @@ pub(crate) mod tokio { use anyhow::Context as _; use easy_ext::ext; use enum_map::{enum_map, EnumMap}; - use futures_util::{future::OptionFuture, TryFutureExt as _}; + use futures_util::{future::OptionFuture, FutureExt as _, TryFutureExt as _}; use ouroboros::self_referencing; use crate::{ @@ -343,6 +343,7 @@ pub(crate) mod tokio { }, }) .join() + .map(InferenceDomainMap::collect) }) .await } From f9066b2a17f3bde5d75ca34ac968d4ef1fc09719 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Mon, 9 Sep 2024 02:02:00 +0900 Subject: [PATCH 07/18] =?UTF-8?q?fixup!=20diff=E3=82=92=E6=8A=91=E3=81=88?= =?UTF-8?q?=E3=82=8B=E5=B7=A5=E5=A4=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/voicevox_core/src/voice_model.rs | 89 +++++++++++++------------ 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/crates/voicevox_core/src/voice_model.rs b/crates/voicevox_core/src/voice_model.rs index 6064d847d..c2d848ed5 100644 --- a/crates/voicevox_core/src/voice_model.rs +++ b/crates/voicevox_core/src/voice_model.rs @@ -448,13 +448,56 @@ pub(crate) mod tokio { } } + type InferenceModelEntries<'manifest> = + (Option>,); + + struct InferenceModelEntry { + indices: EnumMap, + manifest: M, + } + + #[ext] + impl async_zip::base::read::seek::ZipFileReader> { + async fn from_file(path: &Path) -> anyhow::Result + where + Self: Sized, // 自明 + { + let zip = async_fs::File::open(path).await.with_context(|| { + // fs-errのと同じにする + format!("failed to open file `{}`", path.display()) + })?; + let zip = futures_util::io::BufReader::new(zip); + let zip = async_zip::base::read::seek::ZipFileReader::new(zip).await?; + Ok(zip) + } + + fn find_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) + } + } + + #[rustfmt::skip] #[cfg(any())] // FIXME: Gitのdiffを抑えるためだけに残してある状態 + const _: () = { struct AsyncVvmEntry { index: usize, entry: async_zip::ZipEntry, } - #[cfg(any())] // FIXME: Gitのdiffを抑えるためだけに残してある状態 #[derive(new)] struct AsyncVvmEntryReader<'a> { path: &'a Path, @@ -462,7 +505,6 @@ pub(crate) mod tokio { entry_map: HashMap, } - #[cfg(any())] // FIXME: Gitのdiffを抑えるためだけに残してある状態 impl<'a> AsyncVvmEntryReader<'a> { async fn open(path: &'a Path) -> LoadModelResult { let reader = async { @@ -521,48 +563,7 @@ pub(crate) mod tokio { }) } } - - type InferenceModelEntries<'manifest> = - (Option>,); - - struct InferenceModelEntry { - indices: EnumMap, - manifest: M, - } - - #[ext] - impl async_zip::base::read::seek::ZipFileReader> { - async fn from_file(path: &Path) -> anyhow::Result - where - Self: Sized, // 自明 - { - let zip = async_fs::File::open(path).await.with_context(|| { - // fs-errのと同じにする - format!("failed to open file `{}`", path.display()) - })?; - let zip = futures_util::io::BufReader::new(zip); - let zip = async_zip::base::read::seek::ZipFileReader::new(zip).await?; - Ok(zip) - } - - fn find_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) - } - } + }; } #[cfg(test)] From 3de2135fe581a120ff441b7679c0f9aadb61d3a2 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Mon, 9 Sep 2024 20:01:36 +0900 Subject: [PATCH 08/18] =?UTF-8?q?`blocking`=E7=89=88=E3=81=AFblocking?= =?UTF-8?q?=E3=82=AF=E3=83=AC=E3=83=BC=E3=83=88=E3=81=AB=E4=BE=9D=E5=AD=98?= =?UTF-8?q?=E3=81=97=E3=81=AA=E3=81=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/voicevox_core/src/asyncs.rs | 54 +++ crates/voicevox_core/src/lib.rs | 1 + crates/voicevox_core/src/voice_model.rs | 614 ++++++++++-------------- 3 files changed, 317 insertions(+), 352 deletions(-) create mode 100644 crates/voicevox_core/src/asyncs.rs diff --git a/crates/voicevox_core/src/asyncs.rs b/crates/voicevox_core/src/asyncs.rs new file mode 100644 index 000000000..416602a24 --- /dev/null +++ b/crates/voicevox_core/src/asyncs.rs @@ -0,0 +1,54 @@ +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"としての責務を放棄し、すべてをブロックする。 +pub(crate) enum Unstoppable {} + +impl Async for Unstoppable { + async fn open_file(path: impl AsRef) -> io::Result { + return std::fs::File::open(path).map(UnstoppableFile); + + struct UnstoppableFile(std::fs::File); + + impl AsyncRead for UnstoppableFile { + fn poll_read( + mut self: Pin<&mut Self>, + _: &mut task::Context<'_>, + buf: &mut [u8], + ) -> Poll> { + Poll::Ready(self.0.read(buf)) + } + } + + impl AsyncSeek for UnstoppableFile { + fn poll_seek( + mut self: Pin<&mut Self>, + _: &mut task::Context<'_>, + pos: SeekFrom, + ) -> Poll> { + Poll::Ready(self.0.seek(pos)) + } + } + } +} + +/// [blocking]クレートで駆動する。 +/// +/// [blocking](https://docs.rs/crate/blocking) +pub(crate) enum SmolBlocking {} + +impl Async for SmolBlocking { + async fn open_file(path: impl AsRef) -> io::Result { + async_fs::File::open(path).await + } +} diff --git a/crates/voicevox_core/src/lib.rs b/crates/voicevox_core/src/lib.rs index 94ccc0d5a..b31fb035c 100644 --- a/crates/voicevox_core/src/lib.rs +++ b/crates/voicevox_core/src/lib.rs @@ -48,6 +48,7 @@ const _: () = { ); }; +mod asyncs; mod devices; /// cbindgen:ignore mod engine; diff --git a/crates/voicevox_core/src/voice_model.rs b/crates/voicevox_core/src/voice_model.rs index c2d848ed5..4e9d477d4 100644 --- a/crates/voicevox_core/src/voice_model.rs +++ b/crates/voicevox_core/src/voice_model.rs @@ -2,24 +2,34 @@ //! //! 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::enum_map; use enum_map::EnumMap; +use futures_io::{AsyncRead, AsyncSeek}; +use futures_util::future::{FutureExt as _, 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::{InferenceDomainMap, TalkDomain, TalkOperation}, InferenceDomain, }, - manifest::{Manifest, ManifestDomains, StyleIdToInnerVoiceId}, + manifest::{Manifest, ManifestDomains, StyleIdToInnerVoiceId, TalkManifest}, SpeakerMeta, StyleMeta, StyleType, VoiceModelMeta, }; -use std::path::{Path, PathBuf}; /// [`VoiceModelId`]の実体。 /// @@ -51,6 +61,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, +} + +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 { zip.read_file(zip.find_index(MANIFEST_FILENAME)?).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 { + zip.read_file(zip.find_index(manifest.metas_filename())?) + .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_index(&manifest.predict_duration_filename)? + } + TalkOperation::PredictIntonation => zip.find_index( + &manifest.predict_intonation_filename, + )?, + TalkOperation::Decode => { + zip.find_index(&manifest.decode_filename)? + } + }; + + Ok(InferenceModelEntry { indices, manifest }) + }) + .transpose() + .map_err(move |source| LoadModelError { + path: path.to_owned(), + context: LoadModelErrorKind::ReadZipEntry { + filename: MANIFEST_FILENAME.to_owned(), + }, + source: Some(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? + }}; + } + + self.with_inference_model_entries(|inference_model_entries| { + inference_model_entries + .each_ref() + .map(InferenceDomainMap { + talk: |talk| { + let talk = + talk.as_ref() + .map(|InferenceModelEntry { indices, manifest }| { + ( + indices.map(|op, i| (i, manifest[op].clone())), + manifest.style_id_to_inner_voice_id.clone(), + ) + }); + async { + OptionFuture::from(talk.map( + |(entries, style_id_to_inner_voice_id)| async { + 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() + } + }, + }) + .join() + .map(InferenceDomainMap::collect) + }) + .await + } +} + +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); + let zip = async_zip::base::read::seek::ZipFileReader::new(zip).await?; + Ok(zip) + } +} + +#[ext] +impl async_zip::base::read::seek::ZipFileReader { + fn find_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のファイルパスを持っている状態になっている。 /// 音声モデルが持つ、各モデルファイルの実体を除く情報。 /// @@ -145,14 +387,17 @@ pub(crate) mod blocking { use easy_ext::ext; use uuid::Uuid; - use crate::{error::LoadModelResult, infer::domains::InferenceDomainMap, VoiceModelMeta}; + use crate::{ + asyncs::Unstoppable, error::LoadModelResult, infer::domains::InferenceDomainMap, + VoiceModelMeta, + }; - use super::{ModelBytesWithInnerVoiceIdsByDomain, VoiceModelHeader, VoiceModelId}; + use super::{Inner, ModelBytesWithInnerVoiceIdsByDomain, VoiceModelHeader, VoiceModelId}; /// 音声モデル。 /// /// VVMファイルと対応する。 - pub struct VoiceModel(super::tokio::VoiceModel); + pub struct VoiceModel(Inner); impl self::VoiceModel { pub(crate) fn read_inference_models( @@ -163,7 +408,7 @@ pub(crate) mod blocking { /// VVMファイルから`VoiceModel`をコンストラクトする。 pub fn from_path(path: impl AsRef) -> crate::Result { - futures_lite::future::block_on(super::tokio::VoiceModel::from_path(path)).map(Self) + futures_lite::future::block_on(Inner::from_path(path)).map(Self) } /// ID。 @@ -181,57 +426,6 @@ pub(crate) mod blocking { } } - // FIXME: Gitのdiffを抑えるためだけにコメントアウト状態で残してある状態 - //#[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), - // }) - // } - //} - #[ext(IdRef)] pub impl VoiceModel { fn id_ref(&self) -> &Uuid { @@ -241,329 +435,45 @@ pub(crate) mod blocking { } pub(crate) mod tokio { - use std::{path::Path, sync::Arc}; - - use anyhow::Context as _; - use easy_ext::ext; - use enum_map::{enum_map, EnumMap}; - use futures_util::{future::OptionFuture, FutureExt as _, TryFutureExt as _}; - use ouroboros::self_referencing; + use std::path::Path; use crate::{ - error::{LoadModelError, LoadModelErrorKind, LoadModelResult}, - infer::{ - domains::{InferenceDomainMap, TalkDomain, TalkOperation}, - InferenceDomain, - }, - manifest::{Manifest, TalkManifest}, - Result, VoiceModelMeta, + asyncs::SmolBlocking, error::LoadModelResult, infer::domains::InferenceDomainMap, Result, + VoiceModelMeta, }; - use super::{ModelBytesWithInnerVoiceIdsByDomain, VoiceModelHeader, VoiceModelId}; + use super::{Inner, ModelBytesWithInnerVoiceIdsByDomain, VoiceModelHeader, VoiceModelId}; /// 音声モデル。 /// /// VVMファイルと対応する。 - #[self_referencing] - pub struct VoiceModel { - header: VoiceModelHeader, - - #[borrows(header)] - #[not_covariant] - inference_model_entries: InferenceDomainMap>, - } + pub struct VoiceModel(Inner); impl self::VoiceModel { pub(crate) 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 = async_zip::base::read::seek::ZipFileReader::from_file(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? - }}; - } - - self.with_inference_model_entries(|inference_model_entries| { - inference_model_entries - .each_ref() - .map(InferenceDomainMap { - talk: |talk| { - let talk = - talk.as_ref() - .map(|InferenceModelEntry { indices, manifest }| { - ( - indices.map(|op, i| (i, manifest[op].clone())), - manifest.style_id_to_inner_voice_id.clone(), - ) - }); - async { - OptionFuture::from(talk.map( - |(entries, style_id_to_inner_voice_id)| async { - 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() - } - }, - }) - .join() - .map(InferenceDomainMap::collect) - }) - .await + self.0.read_inference_models().await } /// VVMファイルから`VoiceModel`をコンストラクトする。 pub async fn from_path(path: impl AsRef) -> 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 = async_zip::base::read::seek::ZipFileReader::from_file(path) - .await - .map_err(|source| error(LoadModelErrorKind::OpenZipFile, source))?; - - let manifest = &async { zip.read_file(zip.find_index(MANIFEST_FILENAME)?).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 { - zip.read_file(zip.find_index(manifest.metas_filename())?) - .await - } - .await - .map_err(|source| { - error( - LoadModelErrorKind::ReadZipEntry { - filename: manifest.metas_filename().clone(), - }, - source, - ) - })?; - - let header = VoiceModelHeader::new(manifest, metas, path)?; - - VoiceModelTryBuilder { - 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_index(&manifest.predict_duration_filename)? - } - TalkOperation::PredictIntonation => zip.find_index( - &manifest.predict_intonation_filename, - )?, - TalkOperation::Decode => { - zip.find_index(&manifest.decode_filename)? - } - }; - - Ok(InferenceModelEntry { indices, manifest }) - }) - .transpose() - .map_err(move |source| LoadModelError { - path: path.to_owned(), - context: LoadModelErrorKind::ReadZipEntry { - filename: MANIFEST_FILENAME.to_owned(), - }, - source: Some(source), - }) - }, - }) - .collect() - .map_err(crate::Error::from) - }, - } - .try_build() + Inner::from_path(path).await.map(Self) } /// ID。 pub fn id(&self) -> VoiceModelId { - self.borrow_header().manifest.id + self.0.id() } /// メタ情報。 pub fn metas(&self) -> &VoiceModelMeta { - &self.borrow_header().metas + self.0.metas() } pub(crate) fn header(&self) -> &VoiceModelHeader { - self.borrow_header() - } - } - - type InferenceModelEntries<'manifest> = - (Option>,); - - struct InferenceModelEntry { - indices: EnumMap, - manifest: M, - } - - #[ext] - impl async_zip::base::read::seek::ZipFileReader> { - async fn from_file(path: &Path) -> anyhow::Result - where - Self: Sized, // 自明 - { - let zip = async_fs::File::open(path).await.with_context(|| { - // fs-errのと同じにする - format!("failed to open file `{}`", path.display()) - })?; - let zip = futures_util::io::BufReader::new(zip); - let zip = async_zip::base::read::seek::ZipFileReader::new(zip).await?; - Ok(zip) - } - - fn find_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) - } - } - - #[rustfmt::skip] - #[cfg(any())] // FIXME: Gitのdiffを抑えるためだけに残してある状態 - const _: () = { - 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)] From a92d8fff9cdec56d2ee01a94d8580a00d0bb4d84 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Mon, 9 Sep 2024 20:18:44 +0900 Subject: [PATCH 09/18] =?UTF-8?q?async=5Fzip=20v0.0.17=E3=81=AB=E5=82=99?= =?UTF-8?q?=E3=81=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/voicevox_core/src/voice_model.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/voicevox_core/src/voice_model.rs b/crates/voicevox_core/src/voice_model.rs index 4e9d477d4..1c1eece9e 100644 --- a/crates/voicevox_core/src/voice_model.rs +++ b/crates/voicevox_core/src/voice_model.rs @@ -13,7 +13,7 @@ use derive_more::From; use easy_ext::ext; use enum_map::enum_map; use enum_map::EnumMap; -use futures_io::{AsyncRead, AsyncSeek}; +use futures_io::{AsyncBufRead, AsyncSeek}; use futures_util::future::{FutureExt as _, OptionFuture, TryFutureExt as _}; use itertools::Itertools as _; use ouroboros::self_referencing; @@ -260,20 +260,21 @@ struct InferenceModelEntry { impl A { async fn open_zip( path: &Path, - ) -> anyhow::Result> - { + ) -> anyhow::Result< + async_zip::base::read::seek::ZipFileReader, + > { 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); + 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 { +impl async_zip::base::read::seek::ZipFileReader { fn find_index(&self, filename: &str) -> anyhow::Result { let (idx, _) = self .file() From 7b7408ca3e877bd28b7dc663af539970b8a4c3a7 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Tue, 10 Sep 2024 17:05:17 +0900 Subject: [PATCH 10/18] =?UTF-8?q?`SmolBlocking`=20=E2=86=92=20`BlockingThr?= =?UTF-8?q?eadPool`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/voicevox_core/src/asyncs.rs | 4 ++-- crates/voicevox_core/src/voice_model.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/voicevox_core/src/asyncs.rs b/crates/voicevox_core/src/asyncs.rs index 416602a24..638432e34 100644 --- a/crates/voicevox_core/src/asyncs.rs +++ b/crates/voicevox_core/src/asyncs.rs @@ -45,9 +45,9 @@ impl Async for Unstoppable { /// [blocking]クレートで駆動する。 /// /// [blocking](https://docs.rs/crate/blocking) -pub(crate) enum SmolBlocking {} +pub(crate) enum BlockingThreadPool {} -impl Async for SmolBlocking { +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/voice_model.rs b/crates/voicevox_core/src/voice_model.rs index 1c1eece9e..77fb54a13 100644 --- a/crates/voicevox_core/src/voice_model.rs +++ b/crates/voicevox_core/src/voice_model.rs @@ -439,8 +439,8 @@ pub(crate) mod tokio { use std::path::Path; use crate::{ - asyncs::SmolBlocking, error::LoadModelResult, infer::domains::InferenceDomainMap, Result, - VoiceModelMeta, + asyncs::BlockingThreadPool, error::LoadModelResult, infer::domains::InferenceDomainMap, + Result, VoiceModelMeta, }; use super::{Inner, ModelBytesWithInnerVoiceIdsByDomain, VoiceModelHeader, VoiceModelId}; @@ -448,7 +448,7 @@ pub(crate) mod tokio { /// 音声モデル。 /// /// VVMファイルと対応する。 - pub struct VoiceModel(Inner); + pub struct VoiceModel(Inner); impl self::VoiceModel { pub(crate) async fn read_inference_models( From f580e4720ea3a862bf581678cac124e0efbc5f35 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Tue, 10 Sep 2024 17:42:31 +0900 Subject: [PATCH 11/18] Minor refactor --- crates/voicevox_core/src/infer/domains.rs | 8 +-- crates/voicevox_core/src/voice_model.rs | 54 ++++++++++++--------- crates/voicevox_core_macros/src/extract.rs | 8 +-- crates/voicevox_core_macros/src/manifest.rs | 25 ++++++---- 4 files changed, 53 insertions(+), 42 deletions(-) diff --git a/crates/voicevox_core/src/infer/domains.rs b/crates/voicevox_core/src/infer/domains.rs index e9246af70..664732c35 100644 --- a/crates/voicevox_core/src/infer/domains.rs +++ b/crates/voicevox_core/src/infer/domains.rs @@ -20,16 +20,16 @@ pub(crate) struct InferenceDomainMap { impl InferenceDomainMap<(T,)> { pub(crate) fn each_ref(&self) -> InferenceDomainMap<(&T,)> { - InferenceDomainMap { talk: &self.talk } + let talk = &self.talk; + InferenceDomainMap { talk } } pub(crate) fn map T2>( self, fs: InferenceDomainMap<(Ft,)>, ) -> InferenceDomainMap<(T2,)> { - InferenceDomainMap { - talk: (fs.talk)(self.talk), - } + let talk = (fs.talk)(self.talk); + InferenceDomainMap { talk } } } diff --git a/crates/voicevox_core/src/voice_model.rs b/crates/voicevox_core/src/voice_model.rs index 77fb54a13..30807d701 100644 --- a/crates/voicevox_core/src/voice_model.rs +++ b/crates/voicevox_core/src/voice_model.rs @@ -71,7 +71,7 @@ struct Inner { // `_marker`とすると、`borrow__marker`のような名前のメソッドが生成されて`non_snake_case`が // 起動してしまう - marker: PhantomData, + marker: PhantomData A>, } impl Inner { @@ -144,12 +144,13 @@ impl Inner { Ok(InferenceModelEntry { indices, manifest }) }) .transpose() - .map_err(move |source| LoadModelError { - path: path.to_owned(), - context: LoadModelErrorKind::ReadZipEntry { - filename: MANIFEST_FILENAME.to_owned(), - }, - source: Some(source), + .map_err(move |source| { + error( + LoadModelErrorKind::ReadZipEntry { + filename: MANIFEST_FILENAME.to_owned(), + }, + source, + ) }) }, }) @@ -309,27 +310,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 { diff --git a/crates/voicevox_core_macros/src/extract.rs b/crates/voicevox_core_macros/src/extract.rs index a6ced51dd..e9b480630 100644 --- a/crates/voicevox_core_macros/src/extract.rs +++ b/crates/voicevox_core_macros/src/extract.rs @@ -1,8 +1,8 @@ -use syn::{spanned::Spanned as _, Data, DataEnum, DataStruct, DataUnion, Field, Fields, Type}; +use syn::{ + spanned::Spanned as _, Attribute, Data, DataEnum, DataStruct, DataUnion, Field, Fields, Type, +}; -pub(crate) fn struct_fields( - data: &Data, -) -> syn::Result> { +pub(crate) fn struct_fields(data: &Data) -> syn::Result> { let fields = match data { Data::Struct(DataStruct { fields: Fields::Named(fields), diff --git a/crates/voicevox_core_macros/src/manifest.rs b/crates/voicevox_core_macros/src/manifest.rs index e66281130..9560b1fd4 100644 --- a/crates/voicevox_core_macros/src/manifest.rs +++ b/crates/voicevox_core_macros/src/manifest.rs @@ -5,6 +5,8 @@ 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, @@ -16,13 +18,13 @@ pub(crate) fn derive_index_for_fields( let idx = attrs .iter() .find_map(|Attribute { meta, .. }| match meta { - Meta::List(list) if list.path.is_ident("index_for_fields") => Some(list), + Meta::List(list) if list.path.is_ident(ATTR_NAME) => Some(list), _ => None, }) .ok_or_else(|| { syn::Error::new( Span::call_site(), - "missing `#[index_for_fields(…)]` in the struct itself", + format!("missing `#[{ATTR_NAME}(…)]` in the struct itself"), ) })? .parse_args::()?; @@ -31,21 +33,24 @@ pub(crate) fn derive_index_for_fields( let targets = crate::extract::struct_fields(data)? .into_iter() - .flat_map(|(attrs, name, ty)| { - let list = attrs.iter().find_map(|Attribute { meta, .. }| match meta { - Meta::List(list) if list.path.is_ident("index_for_fields") => Some(list), + .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((list, name, ty)) + Some((meta, name, output)) }) - .map(|(list, name, ty)| { - let key = list.parse_args::()?; - Ok((key, name, ty)) + .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(), "no fields have `#[index_for_fields(…)]`") + syn::Error::new( + Span::call_site(), + format!("no fields have `#[{ATTR_NAME}(…)]`"), + ) })?; let arms = targets From 955bea5677d9c6a872acbafbf46d69cd9673cc1a Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Tue, 10 Sep 2024 18:09:01 +0900 Subject: [PATCH 12/18] =?UTF-8?q?`futures=5Flite::future::block=5Fon`?= =?UTF-8?q?=E3=82=92`.block=5Fon()`=E3=81=A8=E3=81=97=E3=81=A6=E4=BD=BF?= =?UTF-8?q?=E3=81=88=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/voicevox_core/src/future.rs | 16 ++++++++++++++++ crates/voicevox_core/src/lib.rs | 1 + crates/voicevox_core/src/voice_model.rs | 8 ++++---- 3 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 crates/voicevox_core/src/future.rs 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/lib.rs b/crates/voicevox_core/src/lib.rs index b31fb035c..dad702cc6 100644 --- a/crates/voicevox_core/src/lib.rs +++ b/crates/voicevox_core/src/lib.rs @@ -53,6 +53,7 @@ mod devices; /// cbindgen:ignore mod engine; mod error; +mod future; mod infer; mod macros; mod manifest; diff --git a/crates/voicevox_core/src/voice_model.rs b/crates/voicevox_core/src/voice_model.rs index 30807d701..a2cc1ddb7 100644 --- a/crates/voicevox_core/src/voice_model.rs +++ b/crates/voicevox_core/src/voice_model.rs @@ -395,8 +395,8 @@ pub(crate) mod blocking { use uuid::Uuid; use crate::{ - asyncs::Unstoppable, error::LoadModelResult, infer::domains::InferenceDomainMap, - VoiceModelMeta, + asyncs::Unstoppable, error::LoadModelResult, future::FutureExt as _, + infer::domains::InferenceDomainMap, VoiceModelMeta, }; use super::{Inner, ModelBytesWithInnerVoiceIdsByDomain, VoiceModelHeader, VoiceModelId}; @@ -410,12 +410,12 @@ pub(crate) mod blocking { pub(crate) fn read_inference_models( &self, ) -> LoadModelResult> { - futures_lite::future::block_on(self.0.read_inference_models()) + self.0.read_inference_models().block_on() } /// VVMファイルから`VoiceModel`をコンストラクトする。 pub fn from_path(path: impl AsRef) -> crate::Result { - futures_lite::future::block_on(Inner::from_path(path)).map(Self) + Inner::from_path(path).block_on().map(Self) } /// ID。 From 5ee2e705f5a0f7ba46ff0788d260e629a239f3fc Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Tue, 10 Sep 2024 20:26:04 +0900 Subject: [PATCH 13/18] =?UTF-8?q?`join`=20=E2=86=92=20`join=5Fall`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/VOICEVOX/voicevox_core/pull/830#discussion_r1751120123 --- crates/voicevox_core/src/infer/domains.rs | 2 +- crates/voicevox_core/src/voice_model.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/voicevox_core/src/infer/domains.rs b/crates/voicevox_core/src/infer/domains.rs index 664732c35..9cae20b2e 100644 --- a/crates/voicevox_core/src/infer/domains.rs +++ b/crates/voicevox_core/src/infer/domains.rs @@ -41,7 +41,7 @@ impl InferenceDomainMap<(Result,)> { } impl InferenceDomainMap<(T,)> { - pub(crate) async fn join(self) -> InferenceDomainMap<(T::Output,)> { + pub(crate) async fn join_all(self) -> InferenceDomainMap<(T::Output,)> { let talk = self.talk.await; InferenceDomainMap { talk } } diff --git a/crates/voicevox_core/src/voice_model.rs b/crates/voicevox_core/src/voice_model.rs index a2cc1ddb7..97aade434 100644 --- a/crates/voicevox_core/src/voice_model.rs +++ b/crates/voicevox_core/src/voice_model.rs @@ -242,7 +242,7 @@ impl Inner { } }, }) - .join() + .join_all() .map(InferenceDomainMap::collect) }) .await From 125ae3bbd8a7affe3525f69d7f51f9868b6e0ce9 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Tue, 10 Sep 2024 21:04:33 +0900 Subject: [PATCH 14/18] =?UTF-8?q?`find=5Findex`=20=E2=86=92=20`find=5Fentr?= =?UTF-8?q?y=5Findex`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/VOICEVOX/voicevox_core/pull/830#discussion_r1751135133 --- crates/voicevox_core/src/voice_model.rs | 35 ++++++++++++++----------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/crates/voicevox_core/src/voice_model.rs b/crates/voicevox_core/src/voice_model.rs index 97aade434..9d66c13a3 100644 --- a/crates/voicevox_core/src/voice_model.rs +++ b/crates/voicevox_core/src/voice_model.rs @@ -90,22 +90,25 @@ impl Inner { .await .map_err(|source| error(LoadModelErrorKind::OpenZipFile, source))?; - let manifest = &async { zip.read_file(zip.find_index(MANIFEST_FILENAME)?).await } - .await - .map_err(|source| { - error( - LoadModelErrorKind::ReadZipEntry { - filename: MANIFEST_FILENAME.to_owned(), - }, - 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 { - zip.read_file(zip.find_index(manifest.metas_filename())?) - .await + let idx = zip.find_entry_index(manifest.metas_filename())?; + zip.read_file(idx).await } .await .map_err(|source| { @@ -131,13 +134,13 @@ impl Inner { .map(|manifest| { let indices = enum_map! { TalkOperation::PredictDuration => { - zip.find_index(&manifest.predict_duration_filename)? + zip.find_entry_index(&manifest.predict_duration_filename)? } - TalkOperation::PredictIntonation => zip.find_index( + TalkOperation::PredictIntonation => zip.find_entry_index( &manifest.predict_intonation_filename, )?, TalkOperation::Decode => { - zip.find_index(&manifest.decode_filename)? + zip.find_entry_index(&manifest.decode_filename)? } }; @@ -276,7 +279,7 @@ impl A { #[ext] impl async_zip::base::read::seek::ZipFileReader { - fn find_index(&self, filename: &str) -> anyhow::Result { + fn find_entry_index(&self, filename: &str) -> anyhow::Result { let (idx, _) = self .file() .entries() From e84fed7f35156845c968b40334506d3e095009fd Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Tue, 10 Sep 2024 23:22:12 +0900 Subject: [PATCH 15/18] =?UTF-8?q?"join"=E3=81=97=E3=81=AA=E3=81=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/voicevox_core/src/infer/domains.rs | 9 --- crates/voicevox_core/src/voice_model.rs | 73 ++++++++++------------- 2 files changed, 32 insertions(+), 50 deletions(-) diff --git a/crates/voicevox_core/src/infer/domains.rs b/crates/voicevox_core/src/infer/domains.rs index 9cae20b2e..5225f2ec3 100644 --- a/crates/voicevox_core/src/infer/domains.rs +++ b/crates/voicevox_core/src/infer/domains.rs @@ -1,7 +1,5 @@ mod talk; -use std::future::Future; - use educe::Educe; use serde::{Deserialize, Deserializer}; @@ -40,13 +38,6 @@ impl InferenceDomainMap<(Result,)> { } } -impl InferenceDomainMap<(T,)> { - pub(crate) async fn join_all(self) -> InferenceDomainMap<(T::Output,)> { - let talk = self.talk.await; - InferenceDomainMap { talk } - } -} - impl<'de, V: InferenceDomainMapValues + ?Sized> Deserialize<'de> for InferenceDomainMap where V::Talk: Deserialize<'de>, diff --git a/crates/voicevox_core/src/voice_model.rs b/crates/voicevox_core/src/voice_model.rs index 9d66c13a3..f7728e84f 100644 --- a/crates/voicevox_core/src/voice_model.rs +++ b/crates/voicevox_core/src/voice_model.rs @@ -14,7 +14,7 @@ use easy_ext::ext; use enum_map::enum_map; use enum_map::EnumMap; use futures_io::{AsyncBufRead, AsyncSeek}; -use futures_util::future::{FutureExt as _, OptionFuture, TryFutureExt as _}; +use futures_util::future::{OptionFuture, TryFutureExt as _}; use itertools::Itertools as _; use ouroboros::self_referencing; use serde::Deserialize; @@ -208,47 +208,39 @@ impl Inner { }}; } - self.with_inference_model_entries(|inference_model_entries| { - inference_model_entries - .each_ref() - .map(InferenceDomainMap { + let InferenceDomainMap { talk } = + self.with_inference_model_entries(|inference_model_entries| { + inference_model_entries.each_ref().map(InferenceDomainMap { talk: |talk| { - let talk = - talk.as_ref() - .map(|InferenceModelEntry { indices, manifest }| { - ( - indices.map(|op, i| (i, manifest[op].clone())), - manifest.style_id_to_inner_voice_id.clone(), - ) - }); - async { - OptionFuture::from(talk.map( - |(entries, style_id_to_inner_voice_id)| async { - 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() - } + talk.as_ref() + .map(|InferenceModelEntry { indices, manifest }| { + ( + indices.map(|op, i| (i, manifest[op].clone())), + manifest.style_id_to_inner_voice_id.clone(), + ) + }) }, }) - .join_all() - .map(InferenceDomainMap::collect) - }) + }); + + 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 }) } } @@ -264,9 +256,8 @@ struct InferenceModelEntry { impl A { async fn open_zip( path: &Path, - ) -> anyhow::Result< - async_zip::base::read::seek::ZipFileReader, - > { + ) -> anyhow::Result> + { let zip = Self::open_file(path).await.with_context(|| { // fs-errのと同じにする format!("failed to open file `{}`", path.display()) From 5cb2bcb228f0c260bfac6fff87f25de58ca5572c Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Wed, 11 Sep 2024 03:28:03 +0900 Subject: [PATCH 16/18] Minor refactor --- crates/voicevox_core/src/asyncs.rs | 2 +- crates/voicevox_core/src/voice_model.rs | 3 +-- crates/voicevox_core_macros/src/lib.rs | 32 ++++++++++++------------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/crates/voicevox_core/src/asyncs.rs b/crates/voicevox_core/src/asyncs.rs index 638432e34..c10384a4f 100644 --- a/crates/voicevox_core/src/asyncs.rs +++ b/crates/voicevox_core/src/asyncs.rs @@ -44,7 +44,7 @@ impl Async for Unstoppable { /// [blocking]クレートで駆動する。 /// -/// [blocking](https://docs.rs/crate/blocking) +/// [blocking]: https://docs.rs/crate/blocking pub(crate) enum BlockingThreadPool {} impl Async for BlockingThreadPool { diff --git a/crates/voicevox_core/src/voice_model.rs b/crates/voicevox_core/src/voice_model.rs index f7728e84f..96a7d4ec4 100644 --- a/crates/voicevox_core/src/voice_model.rs +++ b/crates/voicevox_core/src/voice_model.rs @@ -11,8 +11,7 @@ use std::{ use anyhow::{anyhow, Context as _}; use derive_more::From; use easy_ext::ext; -use enum_map::enum_map; -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 _; diff --git a/crates/voicevox_core_macros/src/lib.rs b/crates/voicevox_core_macros/src/lib.rs index 866a9531b..ff0b83037 100644 --- a/crates/voicevox_core_macros/src/lib.rs +++ b/crates/voicevox_core_macros/src/lib.rs @@ -107,22 +107,22 @@ pub fn derive_inference_output_signature( /// # 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, -// -// // … -// } +/// 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))] From ee7b59340dbe4751374dbd85e05a395821824558 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Wed, 11 Sep 2024 03:57:50 +0900 Subject: [PATCH 17/18] =?UTF-8?q?`crate::asyncs`=E3=81=ABdoc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/VOICEVOX/voicevox_core/pull/830#discussion_r1751106595 --- crates/voicevox_core/src/asyncs.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/voicevox_core/src/asyncs.rs b/crates/voicevox_core/src/asyncs.rs index c10384a4f..b9f5edfdb 100644 --- a/crates/voicevox_core/src/asyncs.rs +++ b/crates/voicevox_core/src/asyncs.rs @@ -1,3 +1,19 @@ +//! 非同期操作の実装の切り替えを行う。 +//! +//! 「[ブロッキング版API]」と「[非同期版API]」との違いはここに集約される +//! …予定。現在は[`crate::voice_model`]のみで利用している。 +//! +//! # Motivation +//! +//! [blocking]クレートで駆動する非同期処理はランタイムが無くても動作する。そのため非同期版APIを +//! もとにブロッキング版APIを構成することはできる。しかし将来WASMビルドすることを考えると、スレッド +//! がまともに扱えないため機能しなくなってしまう。そのためWASM化を見越したブロッキング版APIのため +//! に[`Unstoppable`]を用意している。 +//! +//! [ブロッキング版API]: crate::blocking +//! [非同期版API]: crate::tokio +//! [blocking]: https://docs.rs/crate/blocking + use std::{ io::{self, Read as _, Seek as _, SeekFrom}, path::Path, @@ -12,6 +28,10 @@ pub(crate) trait Async: 'static { } /// "async"としての責務を放棄し、すべてをブロックする。 +/// +/// [ブロッキング版API]用。 +/// +/// [ブロッキング版API]: crate::blocking pub(crate) enum Unstoppable {} impl Async for Unstoppable { @@ -44,7 +64,10 @@ impl Async for Unstoppable { /// [blocking]クレートで駆動する。 /// +/// [非同期版API]用。 +/// /// [blocking]: https://docs.rs/crate/blocking +/// [非同期版API]: crate::tokio pub(crate) enum BlockingThreadPool {} impl Async for BlockingThreadPool { From 18991c1ed5d8e96b37c38b969d3315b012de322f Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Thu, 12 Sep 2024 09:44:11 +0900 Subject: [PATCH 18/18] =?UTF-8?q?`Unstoppable`=20=E2=86=92=20`SingleTasked?= =?UTF-8?q?`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/VOICEVOX/voicevox_core/pull/830#discussion_r1754333231 https://chatgpt.com/share/cdae540e-5751-43a5-a1fb-ac1f17d6a1b8 --- crates/voicevox_core/src/asyncs.rs | 23 ++++++++++++++--------- crates/voicevox_core/src/voice_model.rs | 4 ++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/crates/voicevox_core/src/asyncs.rs b/crates/voicevox_core/src/asyncs.rs index b9f5edfdb..7bbabbb06 100644 --- a/crates/voicevox_core/src/asyncs.rs +++ b/crates/voicevox_core/src/asyncs.rs @@ -8,7 +8,7 @@ //! [blocking]クレートで駆動する非同期処理はランタイムが無くても動作する。そのため非同期版APIを //! もとにブロッキング版APIを構成することはできる。しかし将来WASMビルドすることを考えると、スレッド //! がまともに扱えないため機能しなくなってしまう。そのためWASM化を見越したブロッキング版APIのため -//! に[`Unstoppable`]を用意している。 +//! に[`SingleTasked`]を用意している。 //! //! [ブロッキング版API]: crate::blocking //! [非同期版API]: crate::tokio @@ -27,20 +27,25 @@ pub(crate) trait Async: 'static { async fn open_file(path: impl AsRef) -> io::Result; } -/// "async"としての責務を放棄し、すべてをブロックする。 +/// エグゼキュータが非同期タスクの並行実行をしないことを仮定する、[`Async`]の実装。 /// /// [ブロッキング版API]用。 /// +/// # Performance +/// +/// `async`の中でブロッキング操作を直接行う。そのためTokioやasync-stdのような通常の非同期ランタイム +/// 上で動くべきではない。 +/// /// [ブロッキング版API]: crate::blocking -pub(crate) enum Unstoppable {} +pub(crate) enum SingleTasked {} -impl Async for Unstoppable { +impl Async for SingleTasked { async fn open_file(path: impl AsRef) -> io::Result { - return std::fs::File::open(path).map(UnstoppableFile); + return std::fs::File::open(path).map(BlockingFile); - struct UnstoppableFile(std::fs::File); + struct BlockingFile(std::fs::File); - impl AsyncRead for UnstoppableFile { + impl AsyncRead for BlockingFile { fn poll_read( mut self: Pin<&mut Self>, _: &mut task::Context<'_>, @@ -50,7 +55,7 @@ impl Async for Unstoppable { } } - impl AsyncSeek for UnstoppableFile { + impl AsyncSeek for BlockingFile { fn poll_seek( mut self: Pin<&mut Self>, _: &mut task::Context<'_>, @@ -62,7 +67,7 @@ impl Async for Unstoppable { } } -/// [blocking]クレートで駆動する。 +/// [blocking]クレートで駆動する[`Async`]の実装。 /// /// [非同期版API]用。 /// diff --git a/crates/voicevox_core/src/voice_model.rs b/crates/voicevox_core/src/voice_model.rs index 96a7d4ec4..ac49d2cdb 100644 --- a/crates/voicevox_core/src/voice_model.rs +++ b/crates/voicevox_core/src/voice_model.rs @@ -388,7 +388,7 @@ pub(crate) mod blocking { use uuid::Uuid; use crate::{ - asyncs::Unstoppable, error::LoadModelResult, future::FutureExt as _, + asyncs::SingleTasked, error::LoadModelResult, future::FutureExt as _, infer::domains::InferenceDomainMap, VoiceModelMeta, }; @@ -397,7 +397,7 @@ pub(crate) mod blocking { /// 音声モデル。 /// /// VVMファイルと対応する。 - pub struct VoiceModel(Inner); + pub struct VoiceModel(Inner); impl self::VoiceModel { pub(crate) fn read_inference_models(