diff --git a/Cargo.lock b/Cargo.lock index defbbe093..ae91b89bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4364,6 +4364,7 @@ version = "0.0.0" dependencies = [ "anyhow", "async-fs", + "async-lock", "async_zip", "blocking", "camino", @@ -4486,6 +4487,7 @@ version = "0.0.0" dependencies = [ "camino", "easy-ext", + "futures-lite", "log", "once_cell", "pyo3", @@ -4493,6 +4495,7 @@ dependencies = [ "pyo3-log", "serde", "serde_json", + "tokio", "tracing", "uuid", "voicevox_core", diff --git a/Cargo.toml b/Cargo.toml index 922c7ac09..9226aca03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ anstyle-query = "1.0.0" anyhow = "1.0.65" assert_cmd = "2.0.8" async-fs = "2.1.2" +async-lock = "3.4.0" async_zip = "=0.0.16" bindgen = "0.69.4" binstall-tar = "0.4.39" diff --git a/crates/voicevox_core/Cargo.toml b/crates/voicevox_core/Cargo.toml index e05b04c79..ba508ba44 100644 --- a/crates/voicevox_core/Cargo.toml +++ b/crates/voicevox_core/Cargo.toml @@ -16,7 +16,8 @@ link-onnxruntime = [] [dependencies] anyhow.workspace = true -async-fs.workspace = true +async-fs.workspace = true # 今これを使っている箇所はどこにも無いが、`UserDict`にはこれを使った方がよいはず +async-lock.workspace = true async_zip = { workspace = true, features = ["deflate"] } blocking.workspace = true camino.workspace = true diff --git a/crates/voicevox_core/src/__internal/doctest_fixtures.rs b/crates/voicevox_core/src/__internal/doctest_fixtures.rs index 253bb8d6f..57c28c7a9 100644 --- a/crates/voicevox_core/src/__internal/doctest_fixtures.rs +++ b/crates/voicevox_core/src/__internal/doctest_fixtures.rs @@ -26,7 +26,7 @@ pub async fn synthesizer_with_sample_voice_model( }, )?; - let model = &crate::nonblocking::VoiceModel::from_path(voice_model_path).await?; + let model = &crate::nonblocking::VoiceModelFile::open(voice_model_path).await?; syntesizer.load_voice_model(model).await?; Ok(syntesizer) diff --git a/crates/voicevox_core/src/__internal/interop.rs b/crates/voicevox_core/src/__internal/interop.rs index 677f5515a..a218730cd 100644 --- a/crates/voicevox_core/src/__internal/interop.rs +++ b/crates/voicevox_core/src/__internal/interop.rs @@ -1,3 +1,5 @@ +pub mod raii; + pub use crate::{ metas::merge as merge_metas, synthesizer::blocking::PerformInference, voice_model::blocking::IdRef, diff --git a/crates/voicevox_core/src/__internal/interop/raii.rs b/crates/voicevox_core/src/__internal/interop/raii.rs new file mode 100644 index 000000000..220188ad2 --- /dev/null +++ b/crates/voicevox_core/src/__internal/interop/raii.rs @@ -0,0 +1,43 @@ +use std::{marker::PhantomData, ops::Deref}; + +use ouroboros::self_referencing; + +pub enum MaybeClosed { + Open(T), + Closed, +} + +// [`mapped_lock_guards`]のようなことをやるためのユーティリティ。 +// +// [`mapped_lock_guards`]: https://github.com/rust-lang/rust/issues/117108 +pub fn try_map_guard<'lock, G, F, T, E>(guard: G, f: F) -> Result + 'lock, E> +where + G: 'lock, + F: FnOnce(&G) -> Result<&T, E>, + T: 'lock, +{ + return MappedLockTryBuilder { + guard, + target_builder: f, + marker: PhantomData, + } + .try_build(); + + #[self_referencing] + struct MappedLock<'lock, G: 'lock, T> { + guard: G, + + #[borrows(guard)] + target: &'this T, + + marker: PhantomData<&'lock T>, + } + + impl<'lock, G: 'lock, T: 'lock> Deref for MappedLock<'lock, G, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.borrow_target() + } + } +} diff --git a/crates/voicevox_core/src/asyncs.rs b/crates/voicevox_core/src/asyncs.rs index 5f4d7fd21..d89aa7d4b 100644 --- a/crates/voicevox_core/src/asyncs.rs +++ b/crates/voicevox_core/src/asyncs.rs @@ -15,15 +15,29 @@ use std::{ io::{self, Read as _, Seek as _, SeekFrom}, + ops::DerefMut, path::Path, pin::Pin, task::{self, Poll}, }; +use blocking::Unblock; use futures_io::{AsyncRead, AsyncSeek}; +use futures_util::ready; pub(crate) trait Async: 'static { - async fn open_file(path: impl AsRef) -> io::Result; + type Mutex: Mutex; + type RoFile: AsyncRead + AsyncSeek + Send + Sync + Unpin; + + /// ファイルを読み取り専用(RO)で開く。 + /// + /// `io::Error`は素(`i32`相当)のままにしておき、この関数を呼び出す側でfs-err風のメッセージを付 + /// ける。 + async fn open_file_ro(path: impl AsRef) -> io::Result; +} + +pub(crate) trait Mutex: From + Send + Sync + Unpin { + async fn lock(&self) -> impl DerefMut; } /// エグゼキュータが非同期タスクの並行実行をしないことを仮定する、[`Async`]の実装。 @@ -39,30 +53,47 @@ pub(crate) trait Async: 'static { pub(crate) enum SingleTasked {} impl Async for SingleTasked { - async fn open_file(path: impl AsRef) -> io::Result { - return std::fs::File::open(path).map(BlockingFile); - - struct BlockingFile(std::fs::File); - - impl AsyncRead for BlockingFile { - fn poll_read( - mut self: Pin<&mut Self>, - _: &mut task::Context<'_>, - buf: &mut [u8], - ) -> Poll> { - Poll::Ready(self.0.read(buf)) - } - } + type Mutex = StdMutex; + type RoFile = StdFile; - impl AsyncSeek for BlockingFile { - fn poll_seek( - mut self: Pin<&mut Self>, - _: &mut task::Context<'_>, - pos: SeekFrom, - ) -> Poll> { - Poll::Ready(self.0.seek(pos)) - } - } + async fn open_file_ro(path: impl AsRef) -> io::Result { + std::fs::File::open(path).map(StdFile) + } +} + +pub(crate) struct StdMutex(std::sync::Mutex); + +impl From for StdMutex { + fn from(inner: T) -> Self { + Self(inner.into()) + } +} + +impl Mutex for StdMutex { + async fn lock(&self) -> impl DerefMut { + self.0.lock().unwrap_or_else(|e| panic!("{e}")) + } +} + +pub(crate) struct StdFile(std::fs::File); + +impl AsyncRead for StdFile { + fn poll_read( + mut self: Pin<&mut Self>, + _: &mut task::Context<'_>, + buf: &mut [u8], + ) -> Poll> { + Poll::Ready(self.0.read(buf)) + } +} + +impl AsyncSeek for StdFile { + fn poll_seek( + mut self: Pin<&mut Self>, + _: &mut task::Context<'_>, + pos: SeekFrom, + ) -> Poll> { + Poll::Ready(self.0.seek(pos)) } } @@ -74,7 +105,76 @@ impl Async for SingleTasked { pub(crate) enum BlockingThreadPool {} impl Async for BlockingThreadPool { - async fn open_file(path: impl AsRef) -> io::Result { - async_fs::File::open(path).await + type Mutex = async_lock::Mutex; + type RoFile = AsyncRoFile; + + async fn open_file_ro(path: impl AsRef) -> io::Result { + AsyncRoFile::open(path).await + } +} + +impl Mutex for async_lock::Mutex { + async fn lock(&self) -> impl DerefMut { + self.lock().await + } +} + +// TODO: `async_fs::File::into_std_file`みたいなのがあればこんなの↓は作らなくていいはず。PR出す? +pub(crate) struct AsyncRoFile { + // `poll_read`と`poll_seek`しかしない + unblock: Unblock, + + // async-fsの実装がやっているように「正しい」シーク位置を保持する。ただしファイルはパイプではな + // いことがわかっているため smol-rs/async-fs#4 は考えない + real_seek_pos: Option, +} + +impl AsyncRoFile { + async fn open(path: impl AsRef) -> io::Result { + let path = path.as_ref().to_owned(); + let unblock = Unblock::new(blocking::unblock(|| std::fs::File::open(path)).await?); + Ok(Self { + unblock, + real_seek_pos: None, + }) + } + + pub(crate) async fn close(self) { + let file = self.unblock.into_inner().await; + blocking::unblock(|| drop(file)).await; + } +} + +impl AsyncRead for AsyncRoFile { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + buf: &mut [u8], + ) -> Poll> { + if self.real_seek_pos.is_none() { + self.real_seek_pos = Some(ready!( + Pin::new(&mut self.unblock).poll_seek(cx, SeekFrom::Current(0)) + )?); + } + let n = ready!(Pin::new(&mut self.unblock).poll_read(cx, buf))?; + *self.real_seek_pos.as_mut().expect("should be present") += n as u64; + Poll::Ready(Ok(n)) + } +} + +impl AsyncSeek for AsyncRoFile { + fn poll_seek( + mut self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + pos: SeekFrom, + ) -> Poll> { + // async-fsの実装がやっているような"reposition"を行う。 + // https://github.com/smol-rs/async-fs/issues/2#issuecomment-675595170 + if let Some(real_seek_pos) = self.real_seek_pos { + ready!(Pin::new(&mut self.unblock).poll_seek(cx, SeekFrom::Start(real_seek_pos)))?; + } + self.real_seek_pos = None; + + Pin::new(&mut self.unblock).poll_seek(cx, pos) } } diff --git a/crates/voicevox_core/src/blocking.rs b/crates/voicevox_core/src/blocking.rs index 8d0bc2129..3443e3085 100644 --- a/crates/voicevox_core/src/blocking.rs +++ b/crates/voicevox_core/src/blocking.rs @@ -3,7 +3,7 @@ pub use crate::{ engine::open_jtalk::blocking::OpenJtalk, infer::runtimes::onnxruntime::blocking::Onnxruntime, synthesizer::blocking::Synthesizer, user_dict::dict::blocking::UserDict, - voice_model::blocking::VoiceModel, + voice_model::blocking::VoiceModelFile, }; pub mod onnxruntime { diff --git a/crates/voicevox_core/src/engine/open_jtalk.rs b/crates/voicevox_core/src/engine/open_jtalk.rs index fb7f3ea59..f27e9b0a6 100644 --- a/crates/voicevox_core/src/engine/open_jtalk.rs +++ b/crates/voicevox_core/src/engine/open_jtalk.rs @@ -1,4 +1,4 @@ -// TODO: `VoiceModel`のように、次のような設計にする。 +// TODO: `VoiceModelFile`のように、次のような設計にする。 // // ``` // pub(crate) mod blocking { diff --git a/crates/voicevox_core/src/infer/runtimes/onnxruntime.rs b/crates/voicevox_core/src/infer/runtimes/onnxruntime.rs index 91e435701..f7f92355e 100644 --- a/crates/voicevox_core/src/infer/runtimes/onnxruntime.rs +++ b/crates/voicevox_core/src/infer/runtimes/onnxruntime.rs @@ -1,4 +1,4 @@ -// TODO: `VoiceModel`のように、次のような設計にする。 +// TODO: `VoiceModelFile`のように、次のような設計にする。 // // ``` // pub(crate) mod blocking { diff --git a/crates/voicevox_core/src/nonblocking.rs b/crates/voicevox_core/src/nonblocking.rs index 501a44d04..7187c57fa 100644 --- a/crates/voicevox_core/src/nonblocking.rs +++ b/crates/voicevox_core/src/nonblocking.rs @@ -15,7 +15,7 @@ pub use crate::{ engine::open_jtalk::nonblocking::OpenJtalk, infer::runtimes::onnxruntime::nonblocking::Onnxruntime, synthesizer::nonblocking::Synthesizer, - user_dict::dict::nonblocking::UserDict, voice_model::nonblocking::VoiceModel, + user_dict::dict::nonblocking::UserDict, voice_model::nonblocking::VoiceModelFile, }; pub mod onnxruntime { diff --git a/crates/voicevox_core/src/status.rs b/crates/voicevox_core/src/status.rs index 5103e060e..40e1ae6d2 100644 --- a/crates/voicevox_core/src/status.rs +++ b/crates/voicevox_core/src/status.rs @@ -408,7 +408,7 @@ mod tests { talk: enum_map!(_ => InferenceSessionOptions::new(0, DeviceSpec::Cpu)), }, ); - let model = &crate::nonblocking::VoiceModel::sample().await.unwrap(); + let model = &crate::nonblocking::VoiceModelFile::sample().await.unwrap(); let model_contents = &model.read_inference_models().await.unwrap(); let result = status.insert_model(model.header(), model_contents); assert_debug_fmt_eq!(Ok(()), result); @@ -424,7 +424,7 @@ mod tests { talk: enum_map!(_ => InferenceSessionOptions::new(0, DeviceSpec::Cpu)), }, ); - let vvm = &crate::nonblocking::VoiceModel::sample().await.unwrap(); + let vvm = &crate::nonblocking::VoiceModelFile::sample().await.unwrap(); let model_header = vvm.header(); let model_contents = &vvm.read_inference_models().await.unwrap(); assert!( diff --git a/crates/voicevox_core/src/synthesizer.rs b/crates/voicevox_core/src/synthesizer.rs index 7a1bb2ab8..045a2d9ea 100644 --- a/crates/voicevox_core/src/synthesizer.rs +++ b/crates/voicevox_core/src/synthesizer.rs @@ -1,4 +1,4 @@ -// TODO: `VoiceModel`のように、次のような設計にする。 +// TODO: `VoiceModelFile`のように、次のような設計にする。 // // ``` // pub(crate) mod blocking { @@ -235,7 +235,7 @@ pub(crate) mod blocking { } /// 音声モデルを読み込む。 - pub fn load_voice_model(&self, model: &crate::blocking::VoiceModel) -> Result<()> { + pub fn load_voice_model(&self, model: &crate::blocking::VoiceModelFile) -> Result<()> { let model_bytes = &model.read_inference_models()?; self.status.insert_model(model.header(), model_bytes) } @@ -1181,7 +1181,10 @@ pub(crate) mod nonblocking { self.0.is_gpu_mode() } - pub async fn load_voice_model(&self, model: &crate::nonblocking::VoiceModel) -> Result<()> { + pub async fn load_voice_model( + &self, + model: &crate::nonblocking::VoiceModelFile, + ) -> Result<()> { let model_bytes = &model.read_inference_models().await?; self.0.status.insert_model(model.header(), model_bytes) } @@ -1351,7 +1354,7 @@ mod tests { .unwrap(); let result = syntesizer - .load_voice_model(&crate::nonblocking::VoiceModel::sample().await.unwrap()) + .load_voice_model(&crate::nonblocking::VoiceModelFile::sample().await.unwrap()) .await; assert_debug_fmt_eq!( @@ -1399,7 +1402,7 @@ mod tests { "expected is_model_loaded to return false, but got true", ); syntesizer - .load_voice_model(&crate::nonblocking::VoiceModel::sample().await.unwrap()) + .load_voice_model(&crate::nonblocking::VoiceModelFile::sample().await.unwrap()) .await .unwrap(); @@ -1427,7 +1430,7 @@ mod tests { .unwrap(); syntesizer - .load_voice_model(&crate::nonblocking::VoiceModel::sample().await.unwrap()) + .load_voice_model(&crate::nonblocking::VoiceModelFile::sample().await.unwrap()) .await .unwrap(); @@ -1460,7 +1463,7 @@ mod tests { ) .unwrap(); syntesizer - .load_voice_model(&crate::nonblocking::VoiceModel::sample().await.unwrap()) + .load_voice_model(&crate::nonblocking::VoiceModelFile::sample().await.unwrap()) .await .unwrap(); @@ -1502,7 +1505,7 @@ mod tests { ) .unwrap(); syntesizer - .load_voice_model(&crate::nonblocking::VoiceModel::sample().await.unwrap()) + .load_voice_model(&crate::nonblocking::VoiceModelFile::sample().await.unwrap()) .await .unwrap(); @@ -1599,7 +1602,7 @@ mod tests { ) .unwrap(); - let model = &crate::nonblocking::VoiceModel::sample().await.unwrap(); + let model = &crate::nonblocking::VoiceModelFile::sample().await.unwrap(); syntesizer.load_voice_model(model).await.unwrap(); let query = match input { @@ -1670,7 +1673,7 @@ mod tests { ) .unwrap(); - let model = &crate::nonblocking::VoiceModel::sample().await.unwrap(); + let model = &crate::nonblocking::VoiceModelFile::sample().await.unwrap(); syntesizer.load_voice_model(model).await.unwrap(); let accent_phrases = match input { @@ -1738,7 +1741,7 @@ mod tests { ) .unwrap(); - let model = &crate::nonblocking::VoiceModel::sample().await.unwrap(); + let model = &crate::nonblocking::VoiceModelFile::sample().await.unwrap(); syntesizer.load_voice_model(model).await.unwrap(); let accent_phrases = syntesizer @@ -1801,7 +1804,7 @@ mod tests { ) .unwrap(); - let model = &crate::nonblocking::VoiceModel::sample().await.unwrap(); + let model = &crate::nonblocking::VoiceModelFile::sample().await.unwrap(); syntesizer.load_voice_model(model).await.unwrap(); let accent_phrases = syntesizer @@ -1842,7 +1845,7 @@ mod tests { ) .unwrap(); - let model = &crate::nonblocking::VoiceModel::sample().await.unwrap(); + let model = &crate::nonblocking::VoiceModelFile::sample().await.unwrap(); syntesizer.load_voice_model(model).await.unwrap(); let accent_phrases = syntesizer @@ -1883,7 +1886,7 @@ mod tests { ) .unwrap(); - let model = &crate::nonblocking::VoiceModel::sample().await.unwrap(); + let model = &crate::nonblocking::VoiceModelFile::sample().await.unwrap(); syntesizer.load_voice_model(model).await.unwrap(); let accent_phrases = syntesizer diff --git a/crates/voicevox_core/src/test_util.rs b/crates/voicevox_core/src/test_util.rs index f92c4ee0c..e38f14c5c 100644 --- a/crates/voicevox_core/src/test_util.rs +++ b/crates/voicevox_core/src/test_util.rs @@ -2,8 +2,8 @@ use ::test_util::SAMPLE_VOICE_MODEL_FILE_PATH; use crate::Result; -impl crate::nonblocking::VoiceModel { +impl crate::nonblocking::VoiceModelFile { pub(crate) async fn sample() -> Result { - Self::from_path(SAMPLE_VOICE_MODEL_FILE_PATH).await + Self::open(SAMPLE_VOICE_MODEL_FILE_PATH).await } } diff --git a/crates/voicevox_core/src/user_dict/dict.rs b/crates/voicevox_core/src/user_dict/dict.rs index 13c30540d..0e1c89ca2 100644 --- a/crates/voicevox_core/src/user_dict/dict.rs +++ b/crates/voicevox_core/src/user_dict/dict.rs @@ -1,4 +1,4 @@ -// TODO: `VoiceModel`のように、次のような設計にする。 +// TODO: `VoiceModelFile`のように、次のような設計にする。 // // ``` // pub(crate) mod blocking { diff --git a/crates/voicevox_core/src/voice_model.rs b/crates/voicevox_core/src/voice_model.rs index 48c541439..cf2c0f078 100644 --- a/crates/voicevox_core/src/voice_model.rs +++ b/crates/voicevox_core/src/voice_model.rs @@ -3,7 +3,7 @@ //! VVM ファイルの定義と形式は[ドキュメント](../../../docs/vvm.md)を参照。 use std::{ - marker::PhantomData, + collections::HashMap, path::{Path, PathBuf}, sync::Arc, }; @@ -12,7 +12,7 @@ use anyhow::{anyhow, Context as _}; use derive_more::From; use easy_ext::ext; use enum_map::{enum_map, EnumMap}; -use futures_io::{AsyncBufRead, AsyncSeek}; +use futures_io::{AsyncBufRead, AsyncRead, AsyncSeek}; use futures_util::future::{OptionFuture, TryFutureExt as _}; use itertools::Itertools as _; use ouroboros::self_referencing; @@ -20,7 +20,7 @@ use serde::Deserialize; use uuid::Uuid; use crate::{ - asyncs::Async, + asyncs::{Async, Mutex as _}, error::{LoadModelError, LoadModelErrorKind, LoadModelResult}, infer::{ domains::{InferenceDomainMap, TalkDomain, TalkOperation}, @@ -61,20 +61,18 @@ impl VoiceModelId { } #[self_referencing] -struct Inner { +struct Inner { header: VoiceModelHeader, #[borrows(header)] #[not_covariant] inference_model_entries: InferenceDomainMap>, - // `_marker`とすると、`borrow__marker`のような名前のメソッドが生成されて`non_snake_case`が - // 起動してしまう - marker: PhantomData A>, + zip: A::Mutex, } impl Inner { - async fn from_path(path: impl AsRef) -> crate::Result { + async fn open(path: impl AsRef) -> crate::Result { const MANIFEST_FILENAME: &str = "manifest.json"; let path = path.as_ref(); @@ -89,8 +87,16 @@ impl Inner { .await .map_err(|source| error(LoadModelErrorKind::OpenZipFile, source))?; + let indices = zip.entry_indices_by_utf8_filenames(); + let find_entry_index = |filename: &str| { + indices + .get(filename) + .with_context(|| "could not find `{filename}`") + .copied() + }; + let manifest = &async { - let idx = zip.find_entry_index(MANIFEST_FILENAME)?; + let idx = find_entry_index(MANIFEST_FILENAME)?; zip.read_file(idx).await } .await @@ -106,7 +112,7 @@ impl Inner { .map_err(|source| error(LoadModelErrorKind::InvalidModelFormat, source.into()))?; let metas = &async { - let idx = zip.find_entry_index(manifest.metas_filename())?; + let idx = find_entry_index(manifest.metas_filename())?; zip.read_file(idx).await } .await @@ -133,13 +139,13 @@ impl Inner { .map(|manifest| { let indices = enum_map! { TalkOperation::PredictDuration => { - zip.find_entry_index(&manifest.predict_duration_filename)? + find_entry_index(&manifest.predict_duration_filename)? + } + TalkOperation::PredictIntonation => { + find_entry_index(&manifest.predict_intonation_filename)? } - TalkOperation::PredictIntonation => zip.find_entry_index( - &manifest.predict_intonation_filename, - )?, TalkOperation::Decode => { - zip.find_entry_index(&manifest.decode_filename)? + find_entry_index(&manifest.decode_filename)? } }; @@ -159,7 +165,7 @@ impl Inner { .collect() .map_err(crate::Error::from) }, - marker: PhantomData, + zip: zip.into_inner().into_inner().into(), } .try_build() } @@ -187,9 +193,10 @@ impl Inner { source: Some(source), }; - let mut zip = A::open_zip(path) + let zip = &mut *self.borrow_zip().lock().await; + let mut zip = async_zip::base::read::seek::ZipFileReader::with_bufreader(zip) .await - .map_err(|source| error(LoadModelErrorKind::OpenZipFile, source))?; + .map_err(|source| error(LoadModelErrorKind::OpenZipFile, source.into()))?; macro_rules! read_file { ($entry:expr $(,)?) => {{ @@ -255,29 +262,40 @@ struct InferenceModelEntry { impl A { async fn open_zip( path: &Path, - ) -> anyhow::Result> - { - let zip = Self::open_file(path).await.with_context(|| { + ) -> anyhow::Result< + async_zip::base::read::seek::ZipFileReader>, + > { + let zip = Self::open_file_ro(path).await.with_context(|| { // fs-errのと同じにする format!("failed to open file `{}`", path.display()) })?; - let zip = futures_util::io::BufReader::new(zip); // async_zip v0.0.16では不要、v0.0.17では必要 - let zip = async_zip::base::read::seek::ZipFileReader::new(zip).await?; + let zip = async_zip::base::read::seek::ZipFileReader::with_bufreader(zip).await?; Ok(zip) } } +// `BufReader`はasync_zip v0.0.16では不要、v0.0.17では必要 +#[ext] +impl + async_zip::base::read::seek::ZipFileReader> +{ + async fn with_bufreader(rdr: R) -> async_zip::error::Result + where + Self: Sized, // trivial + { + Self::new(futures_util::io::BufReader::new(rdr)).await + } +} + #[ext] impl async_zip::base::read::seek::ZipFileReader { - fn find_entry_index(&self, filename: &str) -> anyhow::Result { - let (idx, _) = self - .file() + fn entry_indices_by_utf8_filenames(&self) -> HashMap { + self.file() .entries() .iter() .enumerate() - .find(|(_, e)| e.filename().as_str().ok() == Some(filename)) - .with_context(|| "could not find `{filename}`")?; - Ok(idx) + .flat_map(|(i, e)| e.filename().as_str().map(|s| (s.to_owned(), i))) + .collect() } async fn read_file(&mut self, index: usize) -> anyhow::Result> { @@ -394,21 +412,21 @@ pub(crate) mod blocking { use super::{Inner, ModelBytesWithInnerVoiceIdsByDomain, VoiceModelHeader, VoiceModelId}; - /// 音声モデル。 + /// 音声モデルファイル。 /// /// VVMファイルと対応する。 - pub struct VoiceModel(Inner); + pub struct VoiceModelFile(Inner); - impl self::VoiceModel { + impl self::VoiceModelFile { pub(crate) fn read_inference_models( &self, ) -> LoadModelResult> { self.0.read_inference_models().block_on() } - /// VVMファイルから`VoiceModel`をコンストラクトする。 - pub fn from_path(path: impl AsRef) -> crate::Result { - Inner::from_path(path).block_on().map(Self) + /// VVMファイルを開く。 + pub fn open(path: impl AsRef) -> crate::Result { + Inner::open(path).block_on().map(Self) } /// ID。 @@ -427,7 +445,7 @@ pub(crate) mod blocking { } #[ext(IdRef)] - pub impl VoiceModel { + pub impl VoiceModelFile { fn id_ref(&self) -> &Uuid { &self.header().manifest.id.0 } @@ -444,7 +462,7 @@ pub(crate) mod nonblocking { use super::{Inner, ModelBytesWithInnerVoiceIdsByDomain, VoiceModelHeader, VoiceModelId}; - /// 音声モデル。 + /// 音声モデルファイル。 /// /// VVMファイルと対応する。 /// @@ -454,17 +472,23 @@ pub(crate) mod nonblocking { /// /// [blocking]: https://docs.rs/crate/blocking /// [`nonblocking`モジュールのドキュメント]: crate::nonblocking - pub struct VoiceModel(Inner); + pub struct VoiceModelFile(Inner); - impl self::VoiceModel { + impl self::VoiceModelFile { pub(crate) async fn read_inference_models( &self, ) -> LoadModelResult> { self.0.read_inference_models().await } - /// VVMファイルから`VoiceModel`をコンストラクトする。 - pub async fn from_path(path: impl AsRef) -> Result { - Inner::from_path(path).await.map(Self) + + /// VVMファイルを開く。 + pub async fn open(path: impl AsRef) -> Result { + Inner::open(path).await.map(Self) + } + + /// VVMファイルを閉じる。 + pub async fn close(self) { + self.0.into_heads().zip.into_inner().close().await; } /// ID。 diff --git a/crates/voicevox_core_c_api/include/voicevox_core.h b/crates/voicevox_core_c_api/include/voicevox_core.h index 422f32978..e158ebb6a 100644 --- a/crates/voicevox_core_c_api/include/voicevox_core.h +++ b/crates/voicevox_core_c_api/include/voicevox_core.h @@ -295,12 +295,12 @@ typedef struct VoicevoxSynthesizer VoicevoxSynthesizer; typedef struct VoicevoxUserDict VoicevoxUserDict; /** - * 音声モデル。 + * 音声モデルファイル。 * * VVMファイルと対応する。 - * 構築(_construction_)は ::voicevox_voice_model_new_from_path で行い、破棄(_destruction_)は ::voicevox_voice_model_delete で行う。 + * 構築(_construction_)は ::voicevox_voice_model_file_open で行い、破棄(_destruction_)は ::voicevox_voice_model_file_close で行う。 */ -typedef struct VoicevoxVoiceModel VoicevoxVoiceModel; +typedef struct VoicevoxVoiceModelFile VoicevoxVoiceModelFile; #if defined(VOICEVOX_LOAD_ONNXRUNTIME) /** @@ -593,7 +593,7 @@ __declspec(dllimport) const char *voicevox_get_version(void); /** - * VVMファイルから ::VoicevoxVoiceModel を構築(_construct_)する。 + * VVMファイルを開く。 * * @param [in] path vvmファイルへのUTF-8のファイルパス * @param [out] out_model 構築先 @@ -608,56 +608,56 @@ const char *voicevox_get_version(void); #ifdef _WIN32 __declspec(dllimport) #endif -VoicevoxResultCode voicevox_voice_model_new_from_path(const char *path, - struct VoicevoxVoiceModel **out_model); +VoicevoxResultCode voicevox_voice_model_file_open(const char *path, + struct VoicevoxVoiceModelFile **out_model); /** - * ::VoicevoxVoiceModel からIDを取得する。 + * ::VoicevoxVoiceModelFile からIDを取得する。 * * @param [in] model 音声モデル * * @returns 音声モデルID * * \safety{ - * - `model`は ::voicevox_voice_model_new_from_path で得たものでなければならず、また ::voicevox_voice_model_delete で解放されていてはいけない。 + * - `model`は ::voicevox_voice_model_file_open で得たものでなければならず、また ::voicevox_voice_model_file_close で解放されていてはいけない。 * } */ #ifdef _WIN32 __declspec(dllimport) #endif -VoicevoxVoiceModelId voicevox_voice_model_id(const struct VoicevoxVoiceModel *model); +VoicevoxVoiceModelId voicevox_voice_model_file_id(const struct VoicevoxVoiceModelFile *model); /** - * ::VoicevoxVoiceModel からメタ情報を取得する。 + * ::VoicevoxVoiceModelFile からメタ情報を取得する。 * * @param [in] model 音声モデル * * @returns メタ情報のJSON文字列 * * \safety{ - * - `model`は ::voicevox_voice_model_new_from_path で得たものでなければならず、また ::voicevox_voice_model_delete で解放されていてはいけない。 + * - `model`は ::voicevox_voice_model_file_open で得たものでなければならず、また ::voicevox_voice_model_file_close で解放されていてはいけない。 * - 戻り値の文字列の生存期間(_lifetime_)は次にこの関数が呼ばれるか、`model`が破棄されるまでである。この生存期間を越えて文字列にアクセスしてはならない。 * } */ #ifdef _WIN32 __declspec(dllimport) #endif -const char *voicevox_voice_model_get_metas_json(const struct VoicevoxVoiceModel *model); +const char *voicevox_voice_model_file_get_metas_json(const struct VoicevoxVoiceModelFile *model); /** - * ::VoicevoxVoiceModel を破棄(_destruct_)する。 + * ::VoicevoxVoiceModelFile を、所有しているファイルディスクリプタを閉じた上で破棄(_destruct_)する。 * * @param [in] model 破棄対象 * * \safety{ - * - `model`は ::voicevox_voice_model_new_from_path で得たものでなければならず、また既にこの関数で解放されていてはいけない。 + * - `model`は ::voicevox_voice_model_file_open で得たものでなければならず、また既にこの関数で解放されていてはいけない。 * - `model`は以後ダングリングポインタ(_dangling pointer_)として扱われなくてはならない。 * } */ #ifdef _WIN32 __declspec(dllimport) #endif -void voicevox_voice_model_delete(struct VoicevoxVoiceModel *model); +void voicevox_voice_model_file_close(struct VoicevoxVoiceModelFile *model); /** * ::VoicevoxSynthesizer を構築(_construct_)する。 @@ -671,7 +671,7 @@ void voicevox_voice_model_delete(struct VoicevoxVoiceModel *model); * * \safety{ * - `onnxruntime`は ::voicevox_onnxruntime_load_once または ::voicevox_onnxruntime_init_once で得たものでなければならない。 - * - `open_jtalk`は ::voicevox_voice_model_new_from_path で得たものでなければならず、また ::voicevox_open_jtalk_rc_new で解放されていてはいけない。 + * - `open_jtalk`は ::voicevox_voice_model_file_open で得たものでなければならず、また ::voicevox_open_jtalk_rc_new で解放されていてはいけない。 * - `out_synthesizer`は書き込みについて有効でなければならない。 * } */ @@ -708,14 +708,14 @@ void voicevox_synthesizer_delete(struct VoicevoxSynthesizer *synthesizer); * * \safety{ * - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。 - * - `model`は ::voicevox_voice_model_new_from_path で得たものでなければならず、また ::voicevox_voice_model_delete で解放されていてはいけない。 + * - `model`は ::voicevox_voice_model_file_open で得たものでなければならず、また ::voicevox_voice_model_file_close で解放されていてはいけない。 * } */ #ifdef _WIN32 __declspec(dllimport) #endif VoicevoxResultCode voicevox_synthesizer_load_voice_model(const struct VoicevoxSynthesizer *synthesizer, - const struct VoicevoxVoiceModel *model); + const struct VoicevoxVoiceModelFile *model); /** * 音声モデルの読み込みを解除する。 diff --git a/crates/voicevox_core_c_api/src/c_impls.rs b/crates/voicevox_core_c_api/src/c_impls.rs index 1adc402cd..0e9ff9a78 100644 --- a/crates/voicevox_core_c_api/src/c_impls.rs +++ b/crates/voicevox_core_c_api/src/c_impls.rs @@ -5,7 +5,8 @@ use ref_cast::ref_cast_custom; use voicevox_core::{InitializeOptions, Result, VoiceModelId}; use crate::{ - helpers::CApiResult, OpenJtalkRc, VoicevoxOnnxruntime, VoicevoxSynthesizer, VoicevoxVoiceModel, + helpers::CApiResult, OpenJtalkRc, VoicevoxOnnxruntime, VoicevoxSynthesizer, + VoicevoxVoiceModelFile, }; // FIXME: 中身(Rust API)を直接操作するかラッパーメソッド越しにするのかが混在していて、一貫性を @@ -87,7 +88,7 @@ impl VoicevoxSynthesizer { pub(crate) fn load_voice_model( &self, - model: &voicevox_core::blocking::VoiceModel, + model: &voicevox_core::blocking::VoiceModelFile, ) -> CApiResult<()> { self.synthesizer.load_voice_model(model)?; Ok(()) @@ -104,9 +105,9 @@ impl VoicevoxSynthesizer { } } -impl VoicevoxVoiceModel { - pub(crate) fn from_path(path: impl AsRef) -> Result { - let model = voicevox_core::blocking::VoiceModel::from_path(path)?; +impl VoicevoxVoiceModelFile { + pub(crate) fn open(path: impl AsRef) -> Result { + let model = voicevox_core::blocking::VoiceModelFile::open(path)?; let metas = CString::new(serde_json::to_string(model.metas()).unwrap()).unwrap(); Ok(Self { model, metas }) } diff --git a/crates/voicevox_core_c_api/src/compatible_engine.rs b/crates/voicevox_core_c_api/src/compatible_engine.rs index 9fdff0c92..7b1a03e7e 100644 --- a/crates/voicevox_core_c_api/src/compatible_engine.rs +++ b/crates/voicevox_core_c_api/src/compatible_engine.rs @@ -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(Arc::new)) + .map(|entry| voicevox_core::blocking::VoiceModelFile::open(entry.path()).map(Arc::new)) .collect::>() .unwrap() } diff --git a/crates/voicevox_core_c_api/src/lib.rs b/crates/voicevox_core_c_api/src/lib.rs index 161af38e9..63f344553 100644 --- a/crates/voicevox_core_c_api/src/lib.rs +++ b/crates/voicevox_core_c_api/src/lib.rs @@ -399,13 +399,13 @@ pub extern "C" fn voicevox_get_version() -> *const c_char { }; } -/// 音声モデル。 +/// 音声モデルファイル。 /// /// VVMファイルと対応する。 -/// 構築(_construction_)は ::voicevox_voice_model_new_from_path で行い、破棄(_destruction_)は ::voicevox_voice_model_delete で行う。 +/// 構築(_construction_)は ::voicevox_voice_model_file_open で行い、破棄(_destruction_)は ::voicevox_voice_model_file_close で行う。 #[derive(Getters)] -pub struct VoicevoxVoiceModel { - model: voicevox_core::blocking::VoiceModel, +pub struct VoicevoxVoiceModelFile { + model: voicevox_core::blocking::VoiceModelFile, metas: CString, } @@ -417,7 +417,7 @@ pub type VoicevoxVoiceModelId<'a> = &'a [u8; 16]; /// VOICEVOXにおける、ある話者(_speaker_)のあるスタイル(_style_)を指す。 pub type VoicevoxStyleId = u32; -/// VVMファイルから ::VoicevoxVoiceModel を構築(_construct_)する。 +/// VVMファイルを開く。 /// /// @param [in] path vvmファイルへのUTF-8のファイルパス /// @param [out] out_model 構築先 @@ -429,60 +429,64 @@ pub type VoicevoxStyleId = u32; /// - `out_model`は書き込みについて有効でなければならない。 /// } #[no_mangle] -pub unsafe extern "C" fn voicevox_voice_model_new_from_path( +pub unsafe extern "C" fn voicevox_voice_model_file_open( path: *const c_char, - out_model: NonNull>, + out_model: NonNull>, ) -> VoicevoxResultCode { init_logger_once(); into_result_code_with_error((|| { let path = ensure_utf8(CStr::from_ptr(path))?; - let model = VoicevoxVoiceModel::from_path(path)?.into(); + let model = VoicevoxVoiceModelFile::open(path)?.into(); out_model.write_unaligned(model); Ok(()) })()) } -/// ::VoicevoxVoiceModel からIDを取得する。 +/// ::VoicevoxVoiceModelFile からIDを取得する。 /// /// @param [in] model 音声モデル /// /// @returns 音声モデルID /// /// \safety{ -/// - `model`は ::voicevox_voice_model_new_from_path で得たものでなければならず、また ::voicevox_voice_model_delete で解放されていてはいけない。 +/// - `model`は ::voicevox_voice_model_file_open で得たものでなければならず、また ::voicevox_voice_model_file_close で解放されていてはいけない。 /// } #[no_mangle] -pub extern "C" fn voicevox_voice_model_id(model: &VoicevoxVoiceModel) -> VoicevoxVoiceModelId<'_> { +pub extern "C" fn voicevox_voice_model_file_id( + model: &VoicevoxVoiceModelFile, +) -> VoicevoxVoiceModelId<'_> { init_logger_once(); model.model.id_ref().as_bytes() } -/// ::VoicevoxVoiceModel からメタ情報を取得する。 +/// ::VoicevoxVoiceModelFile からメタ情報を取得する。 /// /// @param [in] model 音声モデル /// /// @returns メタ情報のJSON文字列 /// /// \safety{ -/// - `model`は ::voicevox_voice_model_new_from_path で得たものでなければならず、また ::voicevox_voice_model_delete で解放されていてはいけない。 +/// - `model`は ::voicevox_voice_model_file_open で得たものでなければならず、また ::voicevox_voice_model_file_close で解放されていてはいけない。 /// - 戻り値の文字列の生存期間(_lifetime_)は次にこの関数が呼ばれるか、`model`が破棄されるまでである。この生存期間を越えて文字列にアクセスしてはならない。 /// } #[no_mangle] -pub extern "C" fn voicevox_voice_model_get_metas_json(model: &VoicevoxVoiceModel) -> *const c_char { +pub extern "C" fn voicevox_voice_model_file_get_metas_json( + model: &VoicevoxVoiceModelFile, +) -> *const c_char { init_logger_once(); model.metas().as_ptr() } -/// ::VoicevoxVoiceModel を破棄(_destruct_)する。 +/// ::VoicevoxVoiceModelFile を、所有しているファイルディスクリプタを閉じた上で破棄(_destruct_)する。 /// /// @param [in] model 破棄対象 /// /// \safety{ -/// - `model`は ::voicevox_voice_model_new_from_path で得たものでなければならず、また既にこの関数で解放されていてはいけない。 +/// - `model`は ::voicevox_voice_model_file_open で得たものでなければならず、また既にこの関数で解放されていてはいけない。 /// - `model`は以後ダングリングポインタ(_dangling pointer_)として扱われなくてはならない。 /// } #[no_mangle] -pub extern "C" fn voicevox_voice_model_delete(model: Box) { +pub extern "C" fn voicevox_voice_model_file_close(model: Box) { init_logger_once(); drop(model); } @@ -506,7 +510,7 @@ pub struct VoicevoxSynthesizer { /// /// \safety{ /// - `onnxruntime`は ::voicevox_onnxruntime_load_once または ::voicevox_onnxruntime_init_once で得たものでなければならない。 -/// - `open_jtalk`は ::voicevox_voice_model_new_from_path で得たものでなければならず、また ::voicevox_open_jtalk_rc_new で解放されていてはいけない。 +/// - `open_jtalk`は ::voicevox_voice_model_file_open で得たものでなければならず、また ::voicevox_open_jtalk_rc_new で解放されていてはいけない。 /// - `out_synthesizer`は書き込みについて有効でなければならない。 /// } #[no_mangle] @@ -549,12 +553,12 @@ pub extern "C" fn voicevox_synthesizer_delete(synthesizer: Box VoicevoxResultCode { init_logger_once(); into_result_code_with_error(synthesizer.load_voice_model(model.model())) diff --git a/crates/voicevox_core_c_api/tests/e2e/testcases/simple_tts.rs b/crates/voicevox_core_c_api/tests/e2e/testcases/simple_tts.rs index a4381f74d..1997d30e9 100644 --- a/crates/voicevox_core_c_api/tests/e2e/testcases/simple_tts.rs +++ b/crates/voicevox_core_c_api/tests/e2e/testcases/simple_tts.rs @@ -29,7 +29,7 @@ impl assert_cdylib::TestCase for TestCase { let model = { let mut model = MaybeUninit::uninit(); - assert_ok(lib.voicevox_voice_model_new_from_path( + assert_ok(lib.voicevox_voice_model_file_open( c_api::SAMPLE_VOICE_MODEL_FILE_PATH.as_ptr(), model.as_mut_ptr(), )); @@ -88,7 +88,7 @@ impl assert_cdylib::TestCase for TestCase { std::assert_eq!(SNAPSHOTS.output[&self.text].wav_length, wav_length); - lib.voicevox_voice_model_delete(model); + lib.voicevox_voice_model_file_close(model); lib.voicevox_open_jtalk_rc_delete(openjtalk); lib.voicevox_synthesizer_delete(synthesizer); lib.voicevox_wav_free(wav); diff --git a/crates/voicevox_core_c_api/tests/e2e/testcases/synthesizer_new_output_json.rs b/crates/voicevox_core_c_api/tests/e2e/testcases/synthesizer_new_output_json.rs index 9ac8f4b35..ac662d06e 100644 --- a/crates/voicevox_core_c_api/tests/e2e/testcases/synthesizer_new_output_json.rs +++ b/crates/voicevox_core_c_api/tests/e2e/testcases/synthesizer_new_output_json.rs @@ -63,7 +63,7 @@ impl assert_cdylib::TestCase for TestCase { let model = { let mut model = MaybeUninit::uninit(); - assert_ok(lib.voicevox_voice_model_new_from_path( + assert_ok(lib.voicevox_voice_model_file_open( c_api::SAMPLE_VOICE_MODEL_FILE_PATH.as_ptr(), model.as_mut_ptr(), )); diff --git a/crates/voicevox_core_c_api/tests/e2e/testcases/tts_via_audio_query.rs b/crates/voicevox_core_c_api/tests/e2e/testcases/tts_via_audio_query.rs index 0f2ff5fc8..2536a73d3 100644 --- a/crates/voicevox_core_c_api/tests/e2e/testcases/tts_via_audio_query.rs +++ b/crates/voicevox_core_c_api/tests/e2e/testcases/tts_via_audio_query.rs @@ -29,7 +29,7 @@ impl assert_cdylib::TestCase for TestCase { let model = { let mut model = MaybeUninit::uninit(); - assert_ok(lib.voicevox_voice_model_new_from_path( + assert_ok(lib.voicevox_voice_model_file_open( c_api::SAMPLE_VOICE_MODEL_FILE_PATH.as_ptr(), model.as_mut_ptr(), )); @@ -99,7 +99,7 @@ impl assert_cdylib::TestCase for TestCase { std::assert_eq!(SNAPSHOTS.output[&self.text].wav_length, wav_length); - lib.voicevox_voice_model_delete(model); + lib.voicevox_voice_model_file_close(model); lib.voicevox_open_jtalk_rc_delete(openjtalk); lib.voicevox_synthesizer_delete(synthesizer); lib.voicevox_json_free(audio_query); diff --git a/crates/voicevox_core_c_api/tests/e2e/testcases/user_dict_load.rs b/crates/voicevox_core_c_api/tests/e2e/testcases/user_dict_load.rs index 64e062251..d044962ae 100644 --- a/crates/voicevox_core_c_api/tests/e2e/testcases/user_dict_load.rs +++ b/crates/voicevox_core_c_api/tests/e2e/testcases/user_dict_load.rs @@ -47,7 +47,7 @@ impl assert_cdylib::TestCase for TestCase { let model = { let mut model = MaybeUninit::uninit(); - assert_ok(lib.voicevox_voice_model_new_from_path( + assert_ok(lib.voicevox_voice_model_file_open( c_api::SAMPLE_VOICE_MODEL_FILE_PATH.as_ptr(), model.as_mut_ptr(), )); @@ -119,7 +119,7 @@ impl assert_cdylib::TestCase for TestCase { audio_query_with_dict.get("kana") ); - lib.voicevox_voice_model_delete(model); + lib.voicevox_voice_model_file_close(model); lib.voicevox_open_jtalk_rc_delete(openjtalk); lib.voicevox_synthesizer_delete(synthesizer); lib.voicevox_user_dict_delete(dict); diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/Synthesizer.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/Synthesizer.java index 2fac70741..c59f8ca1e 100644 --- a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/Synthesizer.java +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/Synthesizer.java @@ -54,10 +54,11 @@ public boolean isGpuMode() { * @return メタ情報。 */ @Nonnull - public VoiceModel.SpeakerMeta[] metas() { + public VoiceModelFile.SpeakerMeta[] metas() { Gson gson = new Gson(); String metasJson = rsGetMetasJson(); - VoiceModel.SpeakerMeta[] rawMetas = gson.fromJson(metasJson, VoiceModel.SpeakerMeta[].class); + VoiceModelFile.SpeakerMeta[] rawMetas = + gson.fromJson(metasJson, VoiceModelFile.SpeakerMeta[].class); if (rawMetas == null) { throw new NullPointerException("metas"); } @@ -70,7 +71,7 @@ public VoiceModel.SpeakerMeta[] metas() { * @param voiceModel 読み込むモデル。 * @throws InvalidModelDataException 無効なモデルデータの場合。 */ - public void loadVoiceModel(VoiceModel voiceModel) throws InvalidModelDataException { + public void loadVoiceModel(VoiceModelFile voiceModel) throws InvalidModelDataException { rsLoadVoiceModel(voiceModel); } @@ -284,7 +285,7 @@ public TtsConfigurator tts(String text, int styleId) { @Nonnull private native String rsGetMetasJson(); - private native void rsLoadVoiceModel(VoiceModel voiceModel) throws InvalidModelDataException; + private native void rsLoadVoiceModel(VoiceModelFile voiceModel) throws InvalidModelDataException; private native void rsUnloadVoiceModel(UUID voiceModelId); diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/VoiceModel.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/VoiceModelFile.java similarity index 84% rename from crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/VoiceModel.java rename to crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/VoiceModelFile.java index d8c002f0f..b2cceca3f 100644 --- a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/VoiceModel.java +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/VoiceModelFile.java @@ -5,10 +5,11 @@ import com.google.gson.annotations.SerializedName; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import java.io.Closeable; import java.util.UUID; -/** 音声モデル。 */ -public class VoiceModel extends Dll { +/** 音声モデルファイル。 */ +public class VoiceModelFile extends Dll implements Closeable { private long handle; /** ID。 */ @@ -17,8 +18,8 @@ public class VoiceModel extends Dll { /** メタ情報。 */ @Nonnull public final SpeakerMeta[] metas; - public VoiceModel(String modelPath) { - rsFromPath(modelPath); + public VoiceModelFile(String modelPath) { + rsOpen(modelPath); id = rsGetId(); String metasJson = rsGetMetasJson(); Gson gson = new Gson(); @@ -29,12 +30,23 @@ public VoiceModel(String modelPath) { metas = rawMetas; } + /** + * VVMファイルを閉じる。 + * + *

このメソッドが呼ばれた段階で{@link Synthesizer#loadVoiceModel}からのアクセスが継続中の場合、アクセスが終わるまで待つ。 + */ + @Override + public void close() { + rsClose(); + } + + @Override protected void finalize() throws Throwable { rsDrop(); super.finalize(); } - private native void rsFromPath(String modelPath); + private native void rsOpen(String modelPath); @Nonnull private native UUID rsGetId(); @@ -42,6 +54,8 @@ protected void finalize() throws Throwable { @Nonnull private native String rsGetMetasJson(); + private native void rsClose(); + private native void rsDrop(); /** 話者(speaker)のメタ情報。 */ diff --git a/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/MetaTest.java b/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/MetaTest.java index 60df7359f..ece3a87ff 100644 --- a/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/MetaTest.java +++ b/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/MetaTest.java @@ -14,7 +14,8 @@ void checkLoad() { // cwdはvoicevox_core/crates/voicevox_core_java_api/lib String cwd = System.getProperty("user.dir"); File path = new File(cwd + "/../../test_util/data/model/sample.vvm"); - VoiceModel model = new VoiceModel(path.getAbsolutePath()); - assertNotNull(model.metas); + try (VoiceModelFile model = new VoiceModelFile(path.getAbsolutePath())) { + assertNotNull(model.metas); + } } } diff --git a/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/SynthesizerTest.java b/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/SynthesizerTest.java index 0dfa17ea3..4c7d16f56 100644 --- a/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/SynthesizerTest.java +++ b/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/SynthesizerTest.java @@ -49,58 +49,62 @@ boolean checkAllMoras( @Test void checkModel() throws InvalidModelDataException { Onnxruntime onnxruntime = loadOnnxruntime(); - VoiceModel model = loadModel(); OpenJtalk openJtalk = loadOpenJtalk(); Synthesizer synthesizer = Synthesizer.builder(onnxruntime, openJtalk).build(); assertTrue(synthesizer.metas().length == 0); - synthesizer.loadVoiceModel(model); + try (VoiceModelFile model = openModel()) { + synthesizer.loadVoiceModel(model); - assertTrue(synthesizer.metas().length >= 1); - assertTrue(synthesizer.isLoadedVoiceModel(model.id)); + assertTrue(synthesizer.metas().length >= 1); + assertTrue(synthesizer.isLoadedVoiceModel(model.id)); - synthesizer.unloadVoiceModel(model.id); + synthesizer.unloadVoiceModel(model.id); - assertTrue(synthesizer.metas().length == 0); - assertFalse(synthesizer.isLoadedVoiceModel(model.id)); + assertTrue(synthesizer.metas().length == 0); + assertFalse(synthesizer.isLoadedVoiceModel(model.id)); + } } @Test void checkAudioQuery() throws RunModelException, InvalidModelDataException { - VoiceModel model = loadModel(); Onnxruntime onnxruntime = loadOnnxruntime(); OpenJtalk openJtalk = loadOpenJtalk(); Synthesizer synthesizer = Synthesizer.builder(onnxruntime, openJtalk).build(); - synthesizer.loadVoiceModel(model); - AudioQuery query = synthesizer.createAudioQuery("こんにちは", model.metas[0].styles[0].id); - synthesizer.synthesis(query, model.metas[0].styles[0].id).execute(); + try (VoiceModelFile model = openModel()) { + synthesizer.loadVoiceModel(model); + } + + AudioQuery query = synthesizer.createAudioQuery("こんにちは", synthesizer.metas()[0].styles[0].id); + synthesizer.synthesis(query, synthesizer.metas()[0].styles[0].id).execute(); } @Test void checkAccentPhrases() throws RunModelException, InvalidModelDataException { - VoiceModel model = loadModel(); OpenJtalk openJtalk = loadOpenJtalk(); Onnxruntime onnxruntime = loadOnnxruntime(); Synthesizer synthesizer = Synthesizer.builder(onnxruntime, openJtalk).build(); - synthesizer.loadVoiceModel(model); + try (VoiceModelFile model = openModel()) { + synthesizer.loadVoiceModel(model); + } List accentPhrases = - synthesizer.createAccentPhrases("こんにちは", model.metas[0].styles[0].id); + synthesizer.createAccentPhrases("こんにちは", synthesizer.metas()[0].styles[0].id); List accentPhrases2 = - synthesizer.replaceMoraPitch(accentPhrases, model.metas[1].styles[0].id); + synthesizer.replaceMoraPitch(accentPhrases, synthesizer.metas()[1].styles[0].id); assertTrue( checkAllMoras( accentPhrases, accentPhrases2, (mora, otherMora) -> mora.pitch != otherMora.pitch)); List accentPhrases3 = - synthesizer.replacePhonemeLength(accentPhrases, model.metas[1].styles[0].id); + synthesizer.replacePhonemeLength(accentPhrases, synthesizer.metas()[1].styles[0].id); assertTrue( checkAllMoras( accentPhrases, accentPhrases3, (mora, otherMora) -> mora.vowelLength != otherMora.vowelLength)); List accentPhrases4 = - synthesizer.replaceMoraData(accentPhrases, model.metas[1].styles[0].id); + synthesizer.replaceMoraData(accentPhrases, synthesizer.metas()[1].styles[0].id); assertTrue( checkAllMoras( accentPhrases, @@ -111,11 +115,12 @@ void checkAccentPhrases() throws RunModelException, InvalidModelDataException { @Test void checkTts() throws RunModelException, InvalidModelDataException { - VoiceModel model = loadModel(); Onnxruntime onnxruntime = loadOnnxruntime(); OpenJtalk openJtalk = loadOpenJtalk(); Synthesizer synthesizer = Synthesizer.builder(onnxruntime, openJtalk).build(); - synthesizer.loadVoiceModel(model); - synthesizer.tts("こんにちは", model.metas[0].styles[0].id); + try (VoiceModelFile model = openModel()) { + synthesizer.loadVoiceModel(model); + } + synthesizer.tts("こんにちは", synthesizer.metas()[0].styles[0].id); } } diff --git a/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/TestUtils.java b/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/TestUtils.java index 9ab731cd9..f505c327f 100644 --- a/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/TestUtils.java +++ b/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/TestUtils.java @@ -3,13 +3,13 @@ import java.io.File; class TestUtils { - VoiceModel loadModel() { + VoiceModelFile openModel() { // cwdはvoicevox_core/crates/voicevox_core_java_api/lib String cwd = System.getProperty("user.dir"); File path = new File(cwd + "/../../test_util/data/model/sample.vvm"); try { - return new VoiceModel(path.getCanonicalPath()); + return new VoiceModelFile(path.getCanonicalPath()); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/UserDictTest.java b/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/UserDictTest.java index 2fcfc06ab..ed9a94e8e 100644 --- a/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/UserDictTest.java +++ b/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/UserDictTest.java @@ -15,21 +15,24 @@ class UserDictTest extends TestUtils { // 辞書ロード前後でkanaが異なることを確認する @Test void checkLoad() throws RunModelException, InvalidModelDataException, LoadUserDictException { - VoiceModel model = loadModel(); Onnxruntime onnxruntime = loadOnnxruntime(); OpenJtalk openJtalk = loadOpenJtalk(); Synthesizer synthesizer = Synthesizer.builder(onnxruntime, openJtalk).build(); UserDict userDict = new UserDict(); - synthesizer.loadVoiceModel(model); + try (VoiceModelFile model = openModel()) { + synthesizer.loadVoiceModel(model); + } AudioQuery query1 = synthesizer.createAudioQuery( - "this_word_should_not_exist_in_default_dictionary", model.metas[0].styles[0].id); + "this_word_should_not_exist_in_default_dictionary", + synthesizer.metas()[0].styles[0].id); userDict.addWord(new UserDict.Word("this_word_should_not_exist_in_default_dictionary", "テスト")); openJtalk.useUserDict(userDict); AudioQuery query2 = synthesizer.createAudioQuery( - "this_word_should_not_exist_in_default_dictionary", model.metas[0].styles[0].id); + "this_word_should_not_exist_in_default_dictionary", + synthesizer.metas()[0].styles[0].id); assertTrue(query1.kana != query2.kana); } diff --git a/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/VoiceModelTest.java b/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/VoiceModelTest.java index 5a720b07f..2bdba9c28 100644 --- a/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/VoiceModelTest.java +++ b/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/VoiceModelTest.java @@ -17,7 +17,10 @@ class VoiceModelTest extends TestUtils { @Test void idShouldBePreservedAsIs() throws IOException { UUID expected = UUID.fromString(Manifest.readJson().id); - UUID actual = loadModel().id; + UUID actual; + try (VoiceModelFile model = openModel()) { + actual = model.id; + } assertEquals(expected, actual); } diff --git a/crates/voicevox_core_java_api/src/common.rs b/crates/voicevox_core_java_api/src/common.rs index 1b45dd44d..cb2a89a7f 100644 --- a/crates/voicevox_core_java_api/src/common.rs +++ b/crates/voicevox_core_java_api/src/common.rs @@ -1,4 +1,4 @@ -use std::{error::Error as _, iter}; +use std::{error::Error as _, iter, mem, ops::Deref}; use derive_more::From; use easy_ext::ext; @@ -6,7 +6,9 @@ use jni::{ objects::{JObject, JThrowable}, JNIEnv, }; +use tracing::{debug, warn}; use uuid::Uuid; +use voicevox_core::__internal::interop::raii::MaybeClosed; #[macro_export] macro_rules! object { @@ -154,6 +156,9 @@ where env.throw_new("java/lang/IllegalArgumentException", error.to_string()) ) } + JavaApiError::IllegalState(msg) => { + or_panic!(env.throw_new("java/lang/IllegalStateException", msg)) + } }; } fallback @@ -161,6 +166,8 @@ where } } +type JavaApiResult = Result; + #[derive(From, Debug)] pub(crate) enum JavaApiError { #[from] @@ -173,6 +180,69 @@ pub(crate) enum JavaApiError { Uuid(uuid::Error), DeJson(serde_json::Error), + + IllegalState(String), +} + +pub(crate) struct Closable(std::sync::RwLock>); + +impl Closable { + pub(crate) fn new(content: T) -> Self { + Self(MaybeClosed::Open(content).into()) + } + + pub(crate) fn read(&self) -> JavaApiResult + '_> { + let lock = self.0.try_read().map_err(|e| match e { + std::sync::TryLockError::Poisoned(e) => panic!("{e}"), + std::sync::TryLockError::WouldBlock => { + JavaApiError::IllegalState(format!("The `{}` is being closed", T::JAVA_CLASS_IDENT)) + } + })?; + + voicevox_core::__internal::interop::raii::try_map_guard(lock, |lock| match &**lock { + MaybeClosed::Open(content) => Ok(content), + MaybeClosed::Closed => Err(JavaApiError::IllegalState(format!( + "The `{}` is closed", + T::JAVA_CLASS_IDENT, + ))), + }) + } + + pub(crate) fn close(&self) { + let lock = &mut *match self.0.try_write() { + Ok(lock) => lock, + Err(std::sync::TryLockError::Poisoned(e)) => panic!("{e}"), + Err(std::sync::TryLockError::WouldBlock) => { + self.0.write().unwrap_or_else(|e| panic!("{e}")) + } + }; + + if matches!(*lock, MaybeClosed::Open(_)) { + debug!("Closing a `{}`", T::JAVA_CLASS_IDENT); + } + drop(mem::replace(lock, MaybeClosed::Closed)); + } +} + +impl Drop for Closable { + fn drop(&mut self) { + let content = mem::replace( + self.0.get_mut().unwrap_or_else(|e| panic!("{e}")), + MaybeClosed::Closed, + ); + if let MaybeClosed::Open(content) = content { + warn!( + "デストラクタにより`{}`のクローズを行います。通常は、可能な限り`close`でクローズす\ + るようにして下さい", + T::JAVA_CLASS_IDENT, + ); + drop(content); + } + } +} + +pub(crate) trait HasJavaClassIdent { + const JAVA_CLASS_IDENT: &str; } #[ext(JNIEnvExt)] diff --git a/crates/voicevox_core_java_api/src/synthesizer.rs b/crates/voicevox_core_java_api/src/synthesizer.rs index 9ebd98e47..32cdf1200 100644 --- a/crates/voicevox_core_java_api/src/synthesizer.rs +++ b/crates/voicevox_core_java_api/src/synthesizer.rs @@ -107,8 +107,9 @@ unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsLoadVoice ) { throw_if_err(env, (), |env| { let model = env - .get_rust_field::<_, _, Arc>(&model, "handle")? + .get_rust_field::<_, _, crate::voice_model::VoiceModelFile>(&model, "handle")? .clone(); + let model = model.read()?; let internal = env .get_rust_field::<_, _, Arc>>( &this, "handle", diff --git a/crates/voicevox_core_java_api/src/voice_model.rs b/crates/voicevox_core_java_api/src/voice_model.rs index 1ea90ba8c..ef24edbfe 100644 --- a/crates/voicevox_core_java_api/src/voice_model.rs +++ b/crates/voicevox_core_java_api/src/voice_model.rs @@ -1,14 +1,20 @@ use std::{borrow::Cow, sync::Arc}; -use crate::common::{throw_if_err, JNIEnvExt as _}; +use crate::common::{throw_if_err, Closable, HasJavaClassIdent, JNIEnvExt as _}; use jni::{ objects::{JObject, JString}, sys::jobject, JNIEnv, }; +pub(crate) type VoiceModelFile = Arc>; + +impl HasJavaClassIdent for voicevox_core::blocking::VoiceModelFile { + const JAVA_CLASS_IDENT: &str = "VoiceModelFile"; +} + #[no_mangle] -unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_VoiceModel_rsFromPath<'local>( +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_VoiceModelFile_rsOpen<'local>( env: JNIEnv<'local>, this: JObject<'local>, model_path: JString<'local>, @@ -17,23 +23,24 @@ unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_VoiceModel_rsFromPath<' let model_path = env.get_string(&model_path)?; let model_path = &*Cow::from(&model_path); - let internal = voicevox_core::blocking::VoiceModel::from_path(model_path)?; - - env.set_rust_field(&this, "handle", Arc::new(internal))?; + let internal = voicevox_core::blocking::VoiceModelFile::open(model_path)?; + let internal = Arc::new(Closable::new(internal)); + env.set_rust_field(&this, "handle", internal)?; Ok(()) }) } #[no_mangle] -unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_VoiceModel_rsGetId<'local>( +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_VoiceModelFile_rsGetId<'local>( env: JNIEnv<'local>, this: JObject<'local>, ) -> jobject { throw_if_err(env, std::ptr::null_mut(), |env| { let internal = env - .get_rust_field::<_, _, Arc>(&this, "handle")? + .get_rust_field::<_, _, VoiceModelFile>(&this, "handle")? .clone(); + let internal = internal.read()?; let id = env.new_uuid(internal.id().raw_voice_model_id())?; @@ -42,14 +49,15 @@ unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_VoiceModel_rsGetId<'loc } #[no_mangle] -unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_VoiceModel_rsGetMetasJson<'local>( +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_VoiceModelFile_rsGetMetasJson<'local>( env: JNIEnv<'local>, this: JObject<'local>, ) -> jobject { throw_if_err(env, std::ptr::null_mut(), |env| { let internal = env - .get_rust_field::<_, _, Arc>(&this, "handle")? + .get_rust_field::<_, _, VoiceModelFile>(&this, "handle")? .clone(); + let internal = internal.read()?; let metas = internal.metas(); let metas_json = serde_json::to_string(&metas).expect("should not fail"); @@ -58,7 +66,19 @@ unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_VoiceModel_rsGetMetasJs } #[no_mangle] -unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_VoiceModel_rsDrop<'local>( +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_VoiceModelFile_rsClose<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, +) { + throw_if_err(env, (), |env| { + env.take_rust_field::<_, _, VoiceModelFile>(&this, "handle")? + .close(); + Ok(()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_VoiceModelFile_rsDrop<'local>( env: JNIEnv<'local>, this: JObject<'local>, ) { diff --git a/crates/voicevox_core_python_api/Cargo.toml b/crates/voicevox_core_python_api/Cargo.toml index e0877b623..a2c6f7405 100644 --- a/crates/voicevox_core_python_api/Cargo.toml +++ b/crates/voicevox_core_python_api/Cargo.toml @@ -10,6 +10,7 @@ crate-type = ["cdylib"] [dependencies] camino.workspace = true easy-ext.workspace = true +futures-lite.workspace = true log.workspace = true once_cell.workspace = true pyo3 = { workspace = true, features = ["abi3-py38", "extension-module"] } @@ -17,6 +18,7 @@ pyo3-asyncio = { workspace = true, features = ["tokio-runtime"] } pyo3-log.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true +tokio = { workspace = true, features = ["rt", "sync"] } tracing = { workspace = true, features = ["log"] } uuid.workspace = true voicevox_core = { workspace = true, features = ["load-onnxruntime"] } diff --git a/crates/voicevox_core_python_api/python/test/test_asyncio_metas.py b/crates/voicevox_core_python_api/python/test/test_asyncio_metas.py index aea4af999..3b6f857e3 100644 --- a/crates/voicevox_core_python_api/python/test/test_asyncio_metas.py +++ b/crates/voicevox_core_python_api/python/test/test_asyncio_metas.py @@ -7,15 +7,15 @@ import conftest import pytest import pytest_asyncio -from voicevox_core.asyncio import Onnxruntime, OpenJtalk, Synthesizer, VoiceModel +from voicevox_core.asyncio import Onnxruntime, OpenJtalk, Synthesizer, VoiceModelFile -def test_voice_model_metas_works(voice_model: VoiceModel) -> None: +def test_voice_model_metas_works(voice_model: VoiceModelFile) -> None: _ = voice_model.metas @pytest.mark.asyncio -async def test_synthesizer_metas_works(voice_model: VoiceModel) -> None: +async def test_synthesizer_metas_works(voice_model: VoiceModelFile) -> None: synthesizer = Synthesizer( await Onnxruntime.load_once(filename=conftest.onnxruntime_filename), await OpenJtalk.new(conftest.open_jtalk_dic_dir), @@ -25,5 +25,5 @@ async def test_synthesizer_metas_works(voice_model: VoiceModel) -> None: @pytest_asyncio.fixture -async def voice_model() -> VoiceModel: - return await VoiceModel.from_path(conftest.model_dir) +async def voice_model() -> VoiceModelFile: + return await VoiceModelFile.open(conftest.model_dir) diff --git a/crates/voicevox_core_python_api/python/test/test_asyncio_user_dict_load.py b/crates/voicevox_core_python_api/python/test/test_asyncio_user_dict_load.py index d6906a6ac..b6fe50986 100644 --- a/crates/voicevox_core_python_api/python/test/test_asyncio_user_dict_load.py +++ b/crates/voicevox_core_python_api/python/test/test_asyncio_user_dict_load.py @@ -19,7 +19,7 @@ async def test_user_dict_load() -> None: filename=conftest.onnxruntime_filename ) open_jtalk = await voicevox_core.asyncio.OpenJtalk.new(conftest.open_jtalk_dic_dir) - model = await voicevox_core.asyncio.VoiceModel.from_path(conftest.model_dir) + model = await voicevox_core.asyncio.VoiceModelFile.open(conftest.model_dir) synthesizer = voicevox_core.asyncio.Synthesizer(onnxruntime, open_jtalk) await synthesizer.load_voice_model(model) diff --git a/crates/voicevox_core_python_api/python/test/test_blocking_metas.py b/crates/voicevox_core_python_api/python/test/test_blocking_metas.py index 00eade04b..a6aa6441d 100644 --- a/crates/voicevox_core_python_api/python/test/test_blocking_metas.py +++ b/crates/voicevox_core_python_api/python/test/test_blocking_metas.py @@ -6,14 +6,14 @@ import conftest import pytest -from voicevox_core.blocking import Onnxruntime, OpenJtalk, Synthesizer, VoiceModel +from voicevox_core.blocking import Onnxruntime, OpenJtalk, Synthesizer, VoiceModelFile -def test_voice_model_metas_works(voice_model: VoiceModel) -> None: +def test_voice_model_metas_works(voice_model: VoiceModelFile) -> None: _ = voice_model.metas -def test_synthesizer_metas_works(voice_model: VoiceModel) -> None: +def test_synthesizer_metas_works(voice_model: VoiceModelFile) -> None: synthesizer = Synthesizer( Onnxruntime.load_once(filename=conftest.onnxruntime_filename), OpenJtalk(conftest.open_jtalk_dic_dir), @@ -23,5 +23,5 @@ def test_synthesizer_metas_works(voice_model: VoiceModel) -> None: @pytest.fixture -def voice_model() -> VoiceModel: - return VoiceModel.from_path(conftest.model_dir) +def voice_model() -> VoiceModelFile: + return VoiceModelFile.open(conftest.model_dir) diff --git a/crates/voicevox_core_python_api/python/test/test_blocking_user_dict_load.py b/crates/voicevox_core_python_api/python/test/test_blocking_user_dict_load.py index 198becbe2..e8a5bd350 100644 --- a/crates/voicevox_core_python_api/python/test/test_blocking_user_dict_load.py +++ b/crates/voicevox_core_python_api/python/test/test_blocking_user_dict_load.py @@ -17,7 +17,7 @@ def test_user_dict_load() -> None: filename=conftest.onnxruntime_filename ) open_jtalk = voicevox_core.blocking.OpenJtalk(conftest.open_jtalk_dic_dir) - model = voicevox_core.blocking.VoiceModel.from_path(conftest.model_dir) + model = voicevox_core.blocking.VoiceModelFile.open(conftest.model_dir) synthesizer = voicevox_core.blocking.Synthesizer(onnxruntime, open_jtalk) synthesizer.load_voice_model(model) diff --git a/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_asyncio_synthesizer.py b/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_asyncio_synthesizer.py index 26d389477..bfadf8471 100644 --- a/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_asyncio_synthesizer.py +++ b/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_asyncio_synthesizer.py @@ -10,28 +10,32 @@ from voicevox_core.asyncio import Onnxruntime, OpenJtalk, Synthesizer -def test_enter_returns_workable_self(synthesizer: Synthesizer) -> None: - with synthesizer as ctx: +@pytest.mark.asyncio +async def test_enter_returns_workable_self(synthesizer: Synthesizer) -> None: + async with synthesizer as ctx: assert ctx is synthesizer _ = synthesizer.metas -def test_closing_multiple_times_is_allowed(synthesizer: Synthesizer) -> None: - with synthesizer: - with synthesizer: +@pytest.mark.asyncio +async def test_closing_multiple_times_is_allowed(synthesizer: Synthesizer) -> None: + async with synthesizer: + async with synthesizer: pass - synthesizer.close() - synthesizer.close() + await synthesizer.close() + await synthesizer.close() -def test_access_after_close_denied(synthesizer: Synthesizer) -> None: - synthesizer.close() +@pytest.mark.asyncio +async def test_access_after_close_denied(synthesizer: Synthesizer) -> None: + await synthesizer.close() with pytest.raises(ValueError, match="^The `Synthesizer` is closed$"): _ = synthesizer.metas -def test_access_after_exit_denied(synthesizer: Synthesizer) -> None: - with synthesizer: +@pytest.mark.asyncio +async def test_access_after_exit_denied(synthesizer: Synthesizer) -> None: + async with synthesizer: pass with pytest.raises(ValueError, match="^The `Synthesizer` is closed$"): _ = synthesizer.metas diff --git a/crates/voicevox_core_python_api/python/voicevox_core/_rust/asyncio.pyi b/crates/voicevox_core_python_api/python/voicevox_core/_rust/asyncio.pyi index 7652a7d2c..b386d2d5a 100644 --- a/crates/voicevox_core_python_api/python/voicevox_core/_rust/asyncio.pyi +++ b/crates/voicevox_core_python_api/python/voicevox_core/_rust/asyncio.pyi @@ -14,14 +14,14 @@ if TYPE_CHECKING: VoiceModelId, ) -class VoiceModel: +class VoiceModelFile: """ - 音声モデル。""" + 音声モデルファイル。""" @staticmethod - async def from_path(path: Union[str, PathLike[str]]) -> VoiceModel: + async def open(path: Union[str, PathLike[str]]) -> VoiceModelFile: """ - VVMファイルから ``VoiceModel`` を生成する。 + VVMファイルを開く。 Parameters ---------- @@ -29,6 +29,14 @@ class VoiceModel: VVMファイルへのパス。 """ ... + async def close(self) -> None: + """ + VVMファイルを閉じる。 + + このメソッドが呼ばれた段階で :attr:`Synthesizer.load_voice_model` + からのアクセスが継続中の場合、アクセスが終わるまで待つ。 + """ + ... @property def id(self) -> VoiceModelId: """ID。""" @@ -37,6 +45,8 @@ class VoiceModel: def metas(self) -> List[SpeakerMeta]: """メタ情報。""" ... + async def __aenter__(self) -> "VoiceModelFile": ... + async def __aexit__(self, exc_type, exc_value, traceback) -> None: ... class Onnxruntime: """ @@ -160,8 +170,8 @@ class Synthesizer: cpu_num_threads: int = 0, ) -> None: ... def __repr__(self) -> str: ... - def __enter__(self) -> "Synthesizer": ... - def __exit__(self, exc_type, exc_value, traceback) -> None: ... + async def __aenter__(self) -> "Synthesizer": ... + async def __aexit__(self, exc_type, exc_value, traceback) -> None: ... @property def onnxruntime(self) -> Onnxruntime: """ONNX Runtime。""" @@ -174,7 +184,7 @@ class Synthesizer: def metas(self) -> List[SpeakerMeta]: """メタ情報。""" ... - async def load_voice_model(self, model: VoiceModel) -> None: + async def load_voice_model(self, model: VoiceModelFile) -> None: """ モデルを読み込む。 @@ -411,7 +421,7 @@ class Synthesizer: WAVデータ。 """ ... - def close(self) -> None: ... + async def close(self) -> None: ... class UserDict: """ユーザー辞書。""" diff --git a/crates/voicevox_core_python_api/python/voicevox_core/_rust/blocking.pyi b/crates/voicevox_core_python_api/python/voicevox_core/_rust/blocking.pyi index 602ff31bc..891ceb05e 100644 --- a/crates/voicevox_core_python_api/python/voicevox_core/_rust/blocking.pyi +++ b/crates/voicevox_core_python_api/python/voicevox_core/_rust/blocking.pyi @@ -14,14 +14,14 @@ if TYPE_CHECKING: VoiceModelId, ) -class VoiceModel: +class VoiceModelFile: """ - 音声モデル。""" + 音声モデルファイル。""" @staticmethod - def from_path(path: Union[str, PathLike[str]]) -> VoiceModel: + def open(path: Union[str, PathLike[str]]) -> VoiceModelFile: """ - VVMファイルから ``VoiceModel`` を生成する。 + VVMファイルを開く。 Parameters ---------- @@ -29,6 +29,14 @@ class VoiceModel: VVMファイルへのパス。 """ ... + def close(self) -> None: + """ + VVMファイルを閉じる。 + + このメソッドが呼ばれた段階で :attr:`Synthesizer.load_voice_model` + からのアクセスが継続中の場合、アクセスが終わるまで待つ。 + """ + ... @property def id(self) -> VoiceModelId: """ID。""" @@ -37,6 +45,8 @@ class VoiceModel: def metas(self) -> List[SpeakerMeta]: """メタ情報。""" ... + def __enter__(self) -> "VoiceModelFile": ... + def __exit__(self, exc_type, exc_value, traceback) -> None: ... class Onnxruntime: """ @@ -169,7 +179,7 @@ class Synthesizer: def metas(self) -> List[SpeakerMeta]: """メタ情報。""" ... - def load_voice_model(self, model: VoiceModel) -> None: + def load_voice_model(self, model: VoiceModelFile) -> None: """ モデルを読み込む。 diff --git a/crates/voicevox_core_python_api/python/voicevox_core/asyncio.py b/crates/voicevox_core_python_api/python/voicevox_core/asyncio.py index 2cff19cdf..0dc5e0adb 100644 --- a/crates/voicevox_core_python_api/python/voicevox_core/asyncio.py +++ b/crates/voicevox_core_python_api/python/voicevox_core/asyncio.py @@ -1,4 +1,4 @@ # pyright: reportMissingModuleSource=false -from ._rust.asyncio import Onnxruntime, OpenJtalk, Synthesizer, UserDict, VoiceModel +from ._rust.asyncio import Onnxruntime, OpenJtalk, Synthesizer, UserDict, VoiceModelFile -__all__ = ["Onnxruntime", "OpenJtalk", "Synthesizer", "UserDict", "VoiceModel"] +__all__ = ["Onnxruntime", "OpenJtalk", "Synthesizer", "UserDict", "VoiceModelFile"] diff --git a/crates/voicevox_core_python_api/python/voicevox_core/blocking.py b/crates/voicevox_core_python_api/python/voicevox_core/blocking.py index 7fed5fac7..01ea45029 100644 --- a/crates/voicevox_core_python_api/python/voicevox_core/blocking.py +++ b/crates/voicevox_core_python_api/python/voicevox_core/blocking.py @@ -1,4 +1,10 @@ # pyright: reportMissingModuleSource=false -from ._rust.blocking import Onnxruntime, OpenJtalk, Synthesizer, UserDict, VoiceModel +from ._rust.blocking import ( + Onnxruntime, + OpenJtalk, + Synthesizer, + UserDict, + VoiceModelFile, +) -__all__ = ["Onnxruntime", "OpenJtalk", "Synthesizer", "UserDict", "VoiceModel"] +__all__ = ["Onnxruntime", "OpenJtalk", "Synthesizer", "UserDict", "VoiceModelFile"] diff --git a/crates/voicevox_core_python_api/src/convert.rs b/crates/voicevox_core_python_api/src/convert.rs index e57f2fb13..8152bc980 100644 --- a/crates/voicevox_core_python_api/src/convert.rs +++ b/crates/voicevox_core_python_api/src/convert.rs @@ -1,11 +1,11 @@ -use std::{error::Error as _, future::Future, iter, path::PathBuf}; +use std::{error::Error as _, future::Future, iter, panic, path::PathBuf}; use camino::Utf8PathBuf; use easy_ext::ext; use pyo3::{ - exceptions::{PyException, PyValueError}, - types::PyList, - FromPyObject as _, PyAny, PyObject, PyResult, Python, ToPyObject, + exceptions::{PyException, PyRuntimeError, PyValueError}, + types::{IntoPyDict as _, PyList}, + FromPyObject as _, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject, }; use serde::{de::DeserializeOwned, Serialize}; use serde_json::json; @@ -60,16 +60,17 @@ pub(crate) fn from_dataclass(ob: &PyAny) -> PyResult { pub(crate) fn to_pydantic_voice_model_meta<'py>( metas: &VoiceModelMeta, py: Python<'py>, -) -> PyResult> { +) -> PyResult<&'py PyList> { let class = py .import("voicevox_core")? .getattr("SpeakerMeta")? .downcast()?; - metas + let metas = metas .iter() .map(|m| to_pydantic_dataclass(m, class)) - .collect::>>() + .collect::>>()?; + Ok(PyList::new(py, metas)) } pub(crate) fn to_pydantic_dataclass(x: impl Serialize, class: &PyAny) -> PyResult<&PyAny> { @@ -144,7 +145,6 @@ pub(crate) fn to_rust_uuid(ob: &PyAny) -> PyResult { let uuid = ob.getattr("hex")?.extract::()?; uuid.parse::().into_py_value_result() } -// FIXME: `to_object`は必要無いのでは? pub(crate) fn to_py_uuid(py: Python<'_>, uuid: Uuid) -> PyResult { let uuid = uuid.hyphenated().to_string(); let uuid = py.import("uuid")?.call_method1("UUID", (uuid,))?; @@ -176,6 +176,45 @@ pub(crate) fn to_rust_word_type(word_type: &PyAny) -> PyResult serde_json::from_value::(json!(name)).into_py_value_result() } +/// おおよそ以下のコードにおける`f(x)`のようなものを得る。 +/// +/// ```py +/// async def f(x_): +/// return x_ +/// +/// return f(x) +/// ``` +pub(crate) fn ready(x: impl IntoPy, py: Python<'_>) -> PyResult<&PyAny> { + // ```py + // from asyncio import Future + // + // running_loop = asyncio.get_running_loop() + // fut = Future(loop=running_loop) + // fut.set_result(x) + // return fut + // ``` + + let asyncio_future = py.import("asyncio")?.getattr("Future")?; + + let running_loop = pyo3_asyncio::get_running_loop(py)?; + let fut = asyncio_future.call((), Some([("loop", running_loop)].into_py_dict(py)))?; + fut.call_method1("set_result", (x,))?; + Ok(fut) +} + +pub(crate) async fn run_in_executor(f: F) -> PyResult +where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, +{ + tokio::task::spawn_blocking(f) + .await + .map_err(|e| match e.try_into_panic() { + Ok(p) => panic::resume_unwind(p), + Err(e) => PyRuntimeError::new_err(e.to_string()), + }) +} + #[ext(VoicevoxCoreResultExt)] pub(crate) impl voicevox_core::Result { fn into_py_result(self, py: Python<'_>) -> PyResult { diff --git a/crates/voicevox_core_python_api/src/lib.rs b/crates/voicevox_core_python_api/src/lib.rs index c09fafdc8..00b6e7102 100644 --- a/crates/voicevox_core_python_api/src/lib.rs +++ b/crates/voicevox_core_python_api/src/lib.rs @@ -1,16 +1,21 @@ -use std::marker::PhantomData; +use std::{ + marker::PhantomData, + mem, + ops::{Deref, DerefMut}, +}; mod convert; use self::convert::{from_utf8_path, VoicevoxCoreResultExt as _}; use easy_ext::ext; -use log::debug; +use log::{debug, warn}; use pyo3::{ create_exception, exceptions::{PyException, PyKeyError, PyValueError}, pyfunction, pymodule, - types::PyModule, - wrap_pyfunction, PyResult, PyTypeInfo, Python, + types::{PyList, PyModule}, + wrap_pyfunction, Py, PyObject, PyResult, PyTypeInfo, Python, }; +use voicevox_core::__internal::interop::raii::MaybeClosed; #[pymodule] #[pyo3(name = "_rust")] @@ -27,7 +32,7 @@ fn rust(py: Python<'_>, module: &PyModule) -> PyResult<()> { blocking_module.add_class::()?; blocking_module.add_class::()?; blocking_module.add_class::()?; - blocking_module.add_class::()?; + blocking_module.add_class::()?; blocking_module.add_class::()?; module.add_and_register_submodule(blocking_module)?; @@ -35,7 +40,7 @@ fn rust(py: Python<'_>, module: &PyModule) -> PyResult<()> { asyncio_module.add_class::()?; asyncio_module.add_class::()?; asyncio_module.add_class::()?; - asyncio_module.add_class::()?; + asyncio_module.add_class::()?; asyncio_module.add_class::()?; module.add_and_register_submodule(asyncio_module) } @@ -88,48 +93,165 @@ exceptions! { InvalidWordError: PyValueError; } -struct Closable { - content: MaybeClosed, - marker: PhantomData, -} - -enum MaybeClosed { - Open(T), - Closed, +struct Closable { + content: A::RwLock>, + marker: PhantomData<(C, A)>, } -impl Closable { +impl Closable { fn new(content: T) -> Self { Self { - content: MaybeClosed::Open(content), + content: MaybeClosed::Open(content).into(), marker: PhantomData, } } - fn get(&self) -> PyResult<&T> { - match &self.content { + fn read(&self) -> PyResult + '_> { + let lock = self + .content + .try_read_() + .map_err(|_| PyValueError::new_err(format!("The `{}` is being closed", C::NAME)))?; + + voicevox_core::__internal::interop::raii::try_map_guard(lock, |lock| match &**lock { MaybeClosed::Open(content) => Ok(content), MaybeClosed::Closed => Err(PyValueError::new_err(format!( "The `{}` is closed", C::NAME, ))), - } + }) } - fn close(&mut self) { - if matches!(self.content, MaybeClosed::Open(_)) { + async fn close_(&self) -> Option { + let lock = &mut *match self.content.try_write_() { + Ok(lock) => lock, + Err(()) => { + warn!("The `{}` is still in use. Waiting before closing", C::NAME); + self.content.write_().await + } + }; + + if matches!(*lock, MaybeClosed::Open(_)) { debug!("Closing a {}", C::NAME); } - self.content = MaybeClosed::Closed; + match mem::replace(lock, MaybeClosed::Closed) { + MaybeClosed::Open(content) => Some(content), + MaybeClosed::Closed => None, + } + } +} + +impl Closable { + #[must_use = "中身は明示的に`drop`でdropすること"] + fn close(&self) -> Option { + futures_lite::future::block_on(self.close_()) + } +} + +impl Closable { + #[must_use = "中身は明示的に`drop`でdropすること"] + async fn close(&self) -> Option { + self.close_().await } } -impl Drop for Closable { +impl Drop for Closable { fn drop(&mut self) { - self.close(); + let content = mem::replace(self.content.get_mut_(), MaybeClosed::Closed); + if matches!(content, MaybeClosed::Open(_)) { + warn!( + "デストラクタにより`{}`のクローズを行います。通常は、可能な限り`{}`でクローズする\ + ようにして下さい", + C::NAME, + A::EXIT_METHOD, + ); + drop(content); + } } } +trait Async { + const EXIT_METHOD: &str; + type RwLock: RwLock; +} + +enum SingleTasked {} +enum Tokio {} + +impl Async for SingleTasked { + const EXIT_METHOD: &str = "__exit__"; + type RwLock = std::sync::RwLock; +} + +impl Async for Tokio { + const EXIT_METHOD: &str = "__aexit__"; + type RwLock = tokio::sync::RwLock; +} + +trait RwLock: From { + type Item; + type RwLockWriteGuard<'a>: DerefMut + where + Self: 'a; + fn try_read_(&self) -> Result, ()>; + async fn write_(&self) -> Self::RwLockWriteGuard<'_>; + fn try_write_(&self) -> Result, ()>; + fn get_mut_(&mut self) -> &mut Self::Item; +} + +impl RwLock for std::sync::RwLock { + type Item = T; + type RwLockWriteGuard<'a> = std::sync::RwLockWriteGuard<'a, Self::Item> where Self: 'a; + + fn try_read_(&self) -> Result, ()> { + self.try_read().map_err(|e| match e { + std::sync::TryLockError::Poisoned(e) => panic!("{e}"), + std::sync::TryLockError::WouldBlock => (), + }) + } + + async fn write_(&self) -> Self::RwLockWriteGuard<'_> { + self.write().unwrap_or_else(|e| panic!("{e}")) + } + + fn try_write_(&self) -> Result, ()> { + self.try_write().map_err(|e| match e { + std::sync::TryLockError::Poisoned(e) => panic!("{e}"), + std::sync::TryLockError::WouldBlock => (), + }) + } + + fn get_mut_(&mut self) -> &mut Self::Item { + self.get_mut().unwrap_or_else(|e| panic!("{e}")) + } +} + +impl RwLock for tokio::sync::RwLock { + type Item = T; + type RwLockWriteGuard<'a> = tokio::sync::RwLockWriteGuard<'a, Self::Item> where Self: 'a; + + fn try_read_(&self) -> Result, ()> { + self.try_read().map_err(|_| ()) + } + + async fn write_(&self) -> Self::RwLockWriteGuard<'_> { + self.write().await + } + + fn try_write_(&self) -> Result, ()> { + self.try_write().map_err(|_| ()) + } + + fn get_mut_(&mut self) -> &mut Self::Item { + self.get_mut() + } +} + +#[derive(Clone)] +struct VoiceModelFilePyFields { + id: PyObject, // `NewType("VoiceModelId", UUID)` + metas: Py, // `list[SpeakerMeta]` +} + #[pyfunction] fn _validate_pronunciation(pronunciation: &str, py: Python<'_>) -> PyResult<()> { voicevox_core::__internal::validate_pronunciation(pronunciation).into_py_result(py) @@ -155,33 +277,61 @@ mod blocking { UserDictWord, }; - use crate::{convert::VoicevoxCoreResultExt as _, Closable}; + use crate::{ + convert::VoicevoxCoreResultExt as _, Closable, SingleTasked, VoiceModelFilePyFields, + }; #[pyclass] #[derive(Clone)] - pub(crate) struct VoiceModel { - model: Arc, + pub(crate) struct VoiceModelFile { + model: Arc>, + fields: VoiceModelFilePyFields, } #[pymethods] - impl VoiceModel { + impl VoiceModelFile { #[staticmethod] - fn from_path(py: Python<'_>, path: PathBuf) -> PyResult { - let model = voicevox_core::blocking::VoiceModel::from_path(path) - .into_py_result(py)? - .into(); - Ok(Self { model }) + fn open(py: Python<'_>, path: PathBuf) -> PyResult { + let model = voicevox_core::blocking::VoiceModelFile::open(path).into_py_result(py)?; + + let id = crate::convert::to_py_uuid(py, model.id().raw_voice_model_id())?; + let metas = crate::convert::to_pydantic_voice_model_meta(model.metas(), py)?.into(); + + let model = Closable::new(model).into(); + + Ok(Self { + model, + fields: VoiceModelFilePyFields { id, metas }, + }) + } + + fn close(&self) { + let this = self.model.close(); + drop(this); } #[getter] - fn id(&self, py: Python<'_>) -> PyResult { - let id = self.model.id().raw_voice_model_id(); - crate::convert::to_py_uuid(py, id) + fn id(&self) -> PyObject { + self.fields.id.clone() } #[getter] - fn metas<'py>(&self, py: Python<'py>) -> Vec<&'py PyAny> { - crate::convert::to_pydantic_voice_model_meta(self.model.metas(), py).unwrap() + fn metas(&self) -> Py { + self.fields.metas.clone() + } + + fn __enter__(slf: PyRef<'_, Self>) -> PyResult> { + slf.model.read()?; + Ok(slf) + } + + fn __exit__( + &self, + #[expect(unused_variables, reason = "`__exit__`としては必要")] exc_type: &PyAny, + #[expect(unused_variables, reason = "`__exit__`としては必要")] exc_value: &PyAny, + #[expect(unused_variables, reason = "`__exit__`としては必要")] traceback: &PyAny, + ) { + self.close(); } } @@ -279,6 +429,7 @@ mod blocking { synthesizer: Closable< voicevox_core::blocking::Synthesizer, Self, + SingleTasked, >, } @@ -318,7 +469,7 @@ mod blocking { } fn __enter__(slf: PyRef<'_, Self>) -> PyResult> { - slf.synthesizer.get()?; + slf.synthesizer.read()?; Ok(slf) } @@ -338,22 +489,21 @@ mod blocking { #[getter] fn is_gpu_mode(&self) -> PyResult { - let synthesizer = self.synthesizer.get()?; + let synthesizer = self.synthesizer.read()?; Ok(synthesizer.is_gpu_mode()) } #[getter] - fn metas<'py>(&self, py: Python<'py>) -> PyResult> { - let synthesizer = self.synthesizer.get()?; + fn metas<'py>(&self, py: Python<'py>) -> PyResult<&'py PyList> { + let synthesizer = self.synthesizer.read()?; crate::convert::to_pydantic_voice_model_meta(&synthesizer.metas(), py) } fn load_voice_model(&mut self, model: &PyAny, py: Python<'_>) -> PyResult<()> { - let model: VoiceModel = model.extract()?; - self.synthesizer - .get()? - .load_voice_model(&model.model) - .into_py_result(py) + let this = self.synthesizer.read()?; + let model = model.extract::()?; + let model = &model.model.read()?; + this.load_voice_model(model).into_py_result(py) } fn unload_voice_model( @@ -362,7 +512,7 @@ mod blocking { py: Python<'_>, ) -> PyResult<()> { self.synthesizer - .get()? + .read()? .unload_voice_model(voice_model_id.into()) .into_py_result(py) } @@ -373,7 +523,7 @@ mod blocking { ) -> PyResult { Ok(self .synthesizer - .get()? + .read()? .is_loaded_voice_model(voice_model_id.into())) } @@ -383,7 +533,7 @@ mod blocking { style_id: u32, py: Python<'py>, ) -> PyResult<&'py PyAny> { - let synthesizer = self.synthesizer.get()?; + let synthesizer = self.synthesizer.read()?; let audio_query = synthesizer .audio_query_from_kana(kana, StyleId::new(style_id)) @@ -399,7 +549,7 @@ mod blocking { style_id: u32, py: Python<'py>, ) -> PyResult<&'py PyAny> { - let synthesizesr = self.synthesizer.get()?; + let synthesizesr = self.synthesizer.read()?; let audio_query = synthesizesr .audio_query(text, StyleId::new(style_id)) @@ -415,7 +565,7 @@ mod blocking { style_id: u32, py: Python<'py>, ) -> PyResult> { - let synthesizer = self.synthesizer.get()?; + let synthesizer = self.synthesizer.read()?; let accent_phrases = synthesizer .create_accent_phrases_from_kana(kana, StyleId::new(style_id)) @@ -434,7 +584,7 @@ mod blocking { style_id: u32, py: Python<'py>, ) -> PyResult> { - let synthesizer = self.synthesizer.get()?; + let synthesizer = self.synthesizer.read()?; let accent_phrases = synthesizer .create_accent_phrases(text, StyleId::new(style_id)) @@ -453,7 +603,7 @@ mod blocking { style_id: u32, py: Python<'py>, ) -> PyResult> { - let synthesizer = self.synthesizer.get()?; + let synthesizer = self.synthesizer.read()?; crate::convert::blocking_modify_accent_phrases( accent_phrases, StyleId::new(style_id), @@ -468,7 +618,7 @@ mod blocking { style_id: u32, py: Python<'py>, ) -> PyResult> { - let synthesizer = self.synthesizer.get()?; + let synthesizer = self.synthesizer.read()?; crate::convert::blocking_modify_accent_phrases( accent_phrases, StyleId::new(style_id), @@ -483,7 +633,7 @@ mod blocking { style_id: u32, py: Python<'py>, ) -> PyResult> { - let synthesizer = self.synthesizer.get()?; + let synthesizer = self.synthesizer.read()?; crate::convert::blocking_modify_accent_phrases( accent_phrases, StyleId::new(style_id), @@ -506,7 +656,7 @@ mod blocking { ) -> PyResult<&'py PyBytes> { let wav = &self .synthesizer - .get()? + .read()? .synthesis( &audio_query, StyleId::new(style_id), @@ -536,7 +686,7 @@ mod blocking { }; let wav = &self .synthesizer - .get()? + .read()? .tts_from_kana(kana, style_id, options) .into_py_result(py)?; Ok(PyBytes::new(py, wav)) @@ -560,14 +710,14 @@ mod blocking { }; let wav = &self .synthesizer - .get()? + .read()? .tts(text, style_id, options) .into_py_result(py)?; Ok(PyBytes::new(py, wav)) } fn close(&mut self) { - self.synthesizer.close() + drop(self.synthesizer.close()); } } @@ -649,7 +799,7 @@ mod asyncio { use pyo3::{ pyclass, pymethods, types::{IntoPyDict as _, PyBytes, PyDict, PyList}, - Py, PyAny, PyObject, PyRef, PyResult, Python, ToPyObject as _, + Py, PyAny, PyErr, PyObject, PyRef, PyResult, Python, ToPyObject as _, }; use uuid::Uuid; use voicevox_core::{ @@ -657,34 +807,73 @@ mod asyncio { UserDictWord, }; - use crate::{convert::VoicevoxCoreResultExt as _, Closable}; + use crate::{convert::VoicevoxCoreResultExt as _, Closable, Tokio, VoiceModelFilePyFields}; #[pyclass] #[derive(Clone)] - pub(crate) struct VoiceModel { - model: Arc, + pub(crate) struct VoiceModelFile { + model: Arc>, + fields: VoiceModelFilePyFields, } #[pymethods] - impl VoiceModel { + impl VoiceModelFile { #[staticmethod] - fn from_path(py: Python<'_>, path: PathBuf) -> PyResult<&PyAny> { + fn open(py: Python<'_>, path: PathBuf) -> PyResult<&PyAny> { pyo3_asyncio::tokio::future_into_py(py, async move { - let model = voicevox_core::nonblocking::VoiceModel::from_path(path).await; - let model = Python::with_gil(|py| model.into_py_result(py))?.into(); - Ok(Self { model }) + let model = voicevox_core::nonblocking::VoiceModelFile::open(path).await; + let (model, id, metas) = Python::with_gil(|py| { + let model = Python::with_gil(|py| model.into_py_result(py))?; + let id = crate::convert::to_py_uuid(py, model.id().raw_voice_model_id())?; + let metas = + crate::convert::to_pydantic_voice_model_meta(model.metas(), py)?.into(); + Ok::<_, PyErr>((model, id, metas)) + })?; + + let model = Closable::new(model).into(); + + Ok(Self { + model, + fields: VoiceModelFilePyFields { id, metas }, + }) + }) + } + + fn close<'py>(&self, py: Python<'py>) -> PyResult<&'py PyAny> { + let this = self.model.clone(); + pyo3_asyncio::tokio::future_into_py(py, async move { + if let Some(this) = this.close().await { + this.close().await; + } + Ok(()) }) } #[getter] - fn id(&self, py: Python<'_>) -> PyResult { - let id = self.model.id().raw_voice_model_id(); - crate::convert::to_py_uuid(py, id) + fn id(&self) -> PyObject { + self.fields.id.clone() } #[getter] - fn metas<'py>(&self, py: Python<'py>) -> Vec<&'py PyAny> { - crate::convert::to_pydantic_voice_model_meta(self.model.metas(), py).unwrap() + fn metas(&self) -> Py { + self.fields.metas.clone() + } + + fn __aenter__(slf: PyRef<'_, Self>) -> PyResult<&PyAny> { + slf.model.read()?; + + let py = slf.py(); + crate::convert::ready(slf, py) + } + + fn __aexit__<'py>( + &self, + #[expect(unused_variables, reason = "`__aexit__`としては必要")] exc_type: &'py PyAny, + #[expect(unused_variables, reason = "`__aexit__`としては必要")] exc_value: &'py PyAny, + #[expect(unused_variables, reason = "`__aexit__`としては必要")] traceback: &'py PyAny, + py: Python<'py>, + ) -> PyResult<&'py PyAny> { + self.close(py) } } @@ -791,9 +980,12 @@ mod asyncio { #[pyclass] pub(crate) struct Synthesizer { - synthesizer: Closable< - voicevox_core::nonblocking::Synthesizer, - Self, + synthesizer: Arc< + Closable< + voicevox_core::nonblocking::Synthesizer, + Self, + Tokio, + >, >, } @@ -822,7 +1014,7 @@ mod asyncio { }, ); let synthesizer = Python::with_gil(|py| synthesizer.into_py_result(py))?; - let synthesizer = Closable::new(synthesizer); + let synthesizer = Closable::new(synthesizer).into(); Ok(Self { synthesizer }) } @@ -830,18 +1022,21 @@ mod asyncio { "Synthesizer { .. }" } - fn __enter__(slf: PyRef<'_, Self>) -> PyResult> { - slf.synthesizer.get()?; - Ok(slf) + fn __aenter__(slf: PyRef<'_, Self>) -> PyResult<&PyAny> { + slf.synthesizer.read()?; + + let py = slf.py(); + crate::convert::ready(slf, py) } - fn __exit__( + fn __aexit__<'py>( &mut self, - #[expect(unused_variables, reason = "`__exit__`としては必要")] exc_type: &PyAny, - #[expect(unused_variables, reason = "`__exit__`としては必要")] exc_value: &PyAny, - #[expect(unused_variables, reason = "`__exit__`としては必要")] traceback: &PyAny, - ) { - self.close(); + #[expect(unused_variables, reason = "`__aexit__`としては必要")] exc_type: &'py PyAny, + #[expect(unused_variables, reason = "`__aexit__`としては必要")] exc_value: &'py PyAny, + #[expect(unused_variables, reason = "`__aexit__`としては必要")] traceback: &'py PyAny, + py: Python<'py>, + ) -> PyResult<&'py PyAny> { + self.close(py) } #[getter] @@ -851,13 +1046,13 @@ mod asyncio { #[getter] fn is_gpu_mode(&self) -> PyResult { - let synthesizer = self.synthesizer.get()?; + let synthesizer = self.synthesizer.read()?; Ok(synthesizer.is_gpu_mode()) } #[getter] - fn metas<'py>(&self, py: Python<'py>) -> PyResult> { - let synthesizer = self.synthesizer.get()?; + fn metas<'py>(&self, py: Python<'py>) -> PyResult<&'py PyList> { + let synthesizer = self.synthesizer.read()?; crate::convert::to_pydantic_voice_model_meta(&synthesizer.metas(), py) } @@ -866,10 +1061,10 @@ mod asyncio { model: &'py PyAny, py: Python<'py>, ) -> PyResult<&'py PyAny> { - let model: VoiceModel = model.extract()?; - let synthesizer = self.synthesizer.get()?.clone(); + let model: VoiceModelFile = model.extract()?; + let synthesizer = self.synthesizer.read()?.clone(); pyo3_asyncio::tokio::future_into_py(py, async move { - let result = synthesizer.load_voice_model(&model.model).await; + let result = synthesizer.load_voice_model(&*model.model.read()?).await; Python::with_gil(|py| result.into_py_result(py)) }) } @@ -880,7 +1075,7 @@ mod asyncio { py: Python<'_>, ) -> PyResult<()> { self.synthesizer - .get()? + .read()? .unload_voice_model(voice_model_id.into()) .into_py_result(py) } @@ -891,7 +1086,7 @@ mod asyncio { ) -> PyResult { Ok(self .synthesizer - .get()? + .read()? .is_loaded_voice_model(voice_model_id.into())) } @@ -901,7 +1096,7 @@ mod asyncio { style_id: u32, py: Python<'py>, ) -> PyResult<&'py PyAny> { - let synthesizer = self.synthesizer.get()?.clone(); + let synthesizer = self.synthesizer.read()?.clone(); let kana = kana.to_owned(); pyo3_asyncio::tokio::future_into_py_with_locals( py, @@ -929,7 +1124,7 @@ mod asyncio { style_id: u32, py: Python<'py>, ) -> PyResult<&'py PyAny> { - let synthesizer = self.synthesizer.get()?.clone(); + let synthesizer = self.synthesizer.read()?.clone(); let text = text.to_owned(); pyo3_asyncio::tokio::future_into_py_with_locals( py, @@ -953,7 +1148,7 @@ mod asyncio { style_id: u32, py: Python<'py>, ) -> PyResult<&'py PyAny> { - let synthesizer = self.synthesizer.get()?.clone(); + let synthesizer = self.synthesizer.read()?.clone(); let kana = kana.to_owned(); pyo3_asyncio::tokio::future_into_py_with_locals( py, @@ -982,7 +1177,7 @@ mod asyncio { style_id: u32, py: Python<'py>, ) -> PyResult<&'py PyAny> { - let synthesizer = self.synthesizer.get()?.clone(); + let synthesizer = self.synthesizer.read()?.clone(); let text = text.to_owned(); pyo3_asyncio::tokio::future_into_py_with_locals( py, @@ -1011,7 +1206,7 @@ mod asyncio { style_id: u32, py: Python<'py>, ) -> PyResult<&'py PyAny> { - let synthesizer = self.synthesizer.get()?.clone(); + let synthesizer = self.synthesizer.read()?.clone(); crate::convert::async_modify_accent_phrases( accent_phrases, StyleId::new(style_id), @@ -1026,7 +1221,7 @@ mod asyncio { style_id: u32, py: Python<'py>, ) -> PyResult<&'py PyAny> { - let synthesizer = self.synthesizer.get()?.clone(); + let synthesizer = self.synthesizer.read()?.clone(); crate::convert::async_modify_accent_phrases( accent_phrases, StyleId::new(style_id), @@ -1041,7 +1236,7 @@ mod asyncio { style_id: u32, py: Python<'py>, ) -> PyResult<&'py PyAny> { - let synthesizer = self.synthesizer.get()?.clone(); + let synthesizer = self.synthesizer.read()?.clone(); crate::convert::async_modify_accent_phrases( accent_phrases, StyleId::new(style_id), @@ -1058,7 +1253,7 @@ mod asyncio { enable_interrogative_upspeak: bool, py: Python<'py>, ) -> PyResult<&'py PyAny> { - let synthesizer = self.synthesizer.get()?.clone(); + let synthesizer = self.synthesizer.read()?.clone(); pyo3_asyncio::tokio::future_into_py_with_locals( py, pyo3_asyncio::tokio::get_current_locals(py)?, @@ -1096,7 +1291,7 @@ mod asyncio { let options = TtsOptions { enable_interrogative_upspeak, }; - let synthesizer = self.synthesizer.get()?.clone(); + let synthesizer = self.synthesizer.read()?.clone(); let kana = kana.to_owned(); pyo3_asyncio::tokio::future_into_py_with_locals( py, @@ -1128,7 +1323,7 @@ mod asyncio { let options = TtsOptions { enable_interrogative_upspeak, }; - let synthesizer = self.synthesizer.get()?.clone(); + let synthesizer = self.synthesizer.read()?.clone(); let text = text.to_owned(); pyo3_asyncio::tokio::future_into_py_with_locals( py, @@ -1144,8 +1339,14 @@ mod asyncio { ) } - fn close(&mut self) { - self.synthesizer.close() + fn close<'py>(&self, py: Python<'py>) -> PyResult<&'py PyAny> { + let this = self.synthesizer.clone(); + pyo3_asyncio::tokio::future_into_py(py, async move { + if let Some(this) = this.close().await { + crate::convert::run_in_executor(|| drop(this)).await?; + } + Ok(()) + }) } } diff --git a/docs/usage.md b/docs/usage.md index 067250126..26ed50810 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -62,15 +62,15 @@ VOICEVOX コアでは`Synthesizer`に音声モデルを読み込むことでテ ```python from pprint import pprint -from voicevox_core.blocking import Onnxruntime, OpenJtalk, Synthesizer, VoiceModel +from voicevox_core.blocking import Onnxruntime, OpenJtalk, Synthesizer, VoiceModelFile # 1. Synthesizerの初期化 open_jtalk_dict_dir = "open_jtalk_dic_utf_8-1.11" synthesizer = Synthesizer(Onnxruntime.load_once(), OpenJtalk(open_jtalk_dict_dir)) # 2. 音声モデルの読み込み -model = VoiceModel.from_path("model/0.vvm") -synthesizer.load_voice_model(model) +with VoiceModelFile.open("model/0.vvm") as model: + synthesizer.load_voice_model(model) # 3. テキスト音声合成 text = "サンプル音声です" @@ -86,11 +86,11 @@ AIエンジンの`Onnxruntime`のインスタンスと、辞書などを取り ### 2. 音声モデルの読み込み -VVM ファイルから`VoiceModel`インスタンスを作成し、`Synthesizer`に読み込ませます。その VVM ファイルにどの声が含まれているかは`VoiceModel`の`.metas`や[音声モデルと声の対応表](https://github.com/VOICEVOX/voicevox_fat_resource/blob/main/core/model/README.md#%E9%9F%B3%E5%A3%B0%E3%83%A2%E3%83%87%E3%83%ABvvm%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%A8%E5%A3%B0%E3%82%AD%E3%83%A3%E3%83%A9%E3%82%AF%E3%82%BF%E3%83%BC%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB%E5%90%8D%E3%81%A8%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB-id-%E3%81%AE%E5%AF%BE%E5%BF%9C%E8%A1%A8)で確認できます。 +VVM ファイルから`VoiceModelFile`インスタンスを作成し、`Synthesizer`に読み込ませます。その VVM ファイルにどの声が含まれているかは`VoiceModelFile`の`.metas`や[音声モデルと声の対応表](https://github.com/VOICEVOX/voicevox_fat_resource/blob/main/core/model/README.md#%E9%9F%B3%E5%A3%B0%E3%83%A2%E3%83%87%E3%83%ABvvm%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%A8%E5%A3%B0%E3%82%AD%E3%83%A3%E3%83%A9%E3%82%AF%E3%82%BF%E3%83%BC%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB%E5%90%8D%E3%81%A8%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB-id-%E3%81%AE%E5%AF%BE%E5%BF%9C%E8%A1%A8)で確認できます。 ```python -model = VoiceModel.from_path("model/0.vvm") -pprint(model.metas) +with VoiceModelFile.open("model/0.vvm") as model: + pprint(model.metas) ``` ```txt diff --git a/example/cpp/unix/simple_tts.cpp b/example/cpp/unix/simple_tts.cpp index 5db24b12e..210df1549 100644 --- a/example/cpp/unix/simple_tts.cpp +++ b/example/cpp/unix/simple_tts.cpp @@ -47,8 +47,8 @@ int main(int argc, char *argv[]) { if (path.extension() != ".vvm") { continue; } - VoicevoxVoiceModel* model; - result = voicevox_voice_model_new_from_path(path.c_str(), &model); + VoicevoxVoiceModelFile* model; + result = voicevox_voice_model_file_open(path.c_str(), &model); if (result != VoicevoxResultCode::VOICEVOX_RESULT_OK) { std::cerr << voicevox_error_result_to_message(result) << std::endl; return 0; @@ -58,7 +58,7 @@ int main(int argc, char *argv[]) { std::cerr << voicevox_error_result_to_message(result) << std::endl; return 0; } - voicevox_voice_model_delete(model); + voicevox_voice_model_file_close(model); } std::cout << "音声生成中..." << std::endl; diff --git a/example/cpp/windows/simple_tts/simple_tts.cpp b/example/cpp/windows/simple_tts/simple_tts.cpp index 946ef9679..2bdc947c6 100644 --- a/example/cpp/windows/simple_tts/simple_tts.cpp +++ b/example/cpp/windows/simple_tts/simple_tts.cpp @@ -59,9 +59,8 @@ int main() { if (path.extension() != ".vvm") { continue; } - VoicevoxVoiceModel* model; - result = voicevox_voice_model_new_from_path(path.generic_u8string().c_str(), - &model); + VoicevoxVoiceModelFile* model; + result = voicevox_voice_model_file_open(path.generic_u8string().c_str(), &model); if (result != VoicevoxResultCode::VOICEVOX_RESULT_OK) { OutErrorMessage(result); return 0; @@ -71,7 +70,7 @@ int main() { OutErrorMessage(result); return 0; } - voicevox_voice_model_delete(model); + voicevox_voice_model_file_close(model); } std::wcout << L"音声生成中" << std::endl; diff --git a/example/kotlin/app/src/main/kotlin/app/App.kt b/example/kotlin/app/src/main/kotlin/app/App.kt index 7f2651020..0b8d05e33 100644 --- a/example/kotlin/app/src/main/kotlin/app/App.kt +++ b/example/kotlin/app/src/main/kotlin/app/App.kt @@ -47,7 +47,7 @@ fun main(args: Array) { .build() println("Loading: ${vvmPath}") - val vvm = VoiceModel(vvmPath) + val vvm = VoiceModelFile(vvmPath) synthesizer.loadVoiceModel(vvm) println("Creating an AudioQuery from the text: ${text}") diff --git a/example/python/run-asyncio.py b/example/python/run-asyncio.py index b75509183..176ac290f 100644 --- a/example/python/run-asyncio.py +++ b/example/python/run-asyncio.py @@ -9,7 +9,7 @@ from typing import Tuple from voicevox_core import AccelerationMode, AudioQuery -from voicevox_core.asyncio import Onnxruntime, OpenJtalk, Synthesizer, VoiceModel +from voicevox_core.asyncio import Onnxruntime, OpenJtalk, Synthesizer, VoiceModelFile async def main() -> None: @@ -45,8 +45,8 @@ async def main() -> None: logger.debug("%s", f"{synthesizer.is_gpu_mode=}") logger.info("%s", f"Loading `{vvm_path}`") - model = await VoiceModel.from_path(vvm_path) - await synthesizer.load_voice_model(model) + async with await VoiceModelFile.open(vvm_path) as model: + await synthesizer.load_voice_model(model) logger.info("%s", f"Creating an AudioQuery from {text!r}") audio_query = await synthesizer.audio_query(text, style_id) diff --git a/example/python/run.py b/example/python/run.py index 3a9fdd9e7..5f11a1a62 100644 --- a/example/python/run.py +++ b/example/python/run.py @@ -6,7 +6,7 @@ from typing import Tuple from voicevox_core import AccelerationMode, AudioQuery -from voicevox_core.blocking import Onnxruntime, OpenJtalk, Synthesizer, VoiceModel +from voicevox_core.blocking import Onnxruntime, OpenJtalk, Synthesizer, VoiceModelFile def main() -> None: @@ -42,8 +42,8 @@ def main() -> None: logger.debug("%s", f"{synthesizer.is_gpu_mode=}") logger.info("%s", f"Loading `{vvm_path}`") - model = VoiceModel.from_path(vvm_path) - synthesizer.load_voice_model(model) + with VoiceModelFile.open(vvm_path) as model: + synthesizer.load_voice_model(model) logger.info("%s", f"Creating an AudioQuery from {text!r}") audio_query = synthesizer.audio_query(text, style_id)