From 237ffaf526791ce47f097a0b6e0524ee074691ce Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Sun, 30 Jul 2023 13:30:16 +0900 Subject: [PATCH 1/8] =?UTF-8?q?Python=20API=E3=81=AE`Synthesizer`=E3=82=92?= =?UTF-8?q?`close()`=E5=8F=AF=E8=83=BD=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../python/voicevox_core/_rust.pyi | 3 + crates/voicevox_core_python_api/src/lib.rs | 112 +++++++++++++----- 2 files changed, 87 insertions(+), 28 deletions(-) diff --git a/crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi b/crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi index 0a444036c..2f2528785 100644 --- a/crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi +++ b/crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi @@ -77,6 +77,8 @@ class Synthesizer: """ ... def __repr__(self) -> str: ... + def __enter__(self) -> "Synthesizer": ... + def __exit__(self, exc_type, exc_value, traceback): ... @property def is_gpu_mode(self) -> bool: """ハードウェアアクセラレーションがGPUモードか判定する。 @@ -257,6 +259,7 @@ class Synthesizer: 疑問文の調整を有効にする。 """ ... + def close(self) -> None: ... class UserDict: """ユーザー辞書。 diff --git a/crates/voicevox_core_python_api/src/lib.rs b/crates/voicevox_core_python_api/src/lib.rs index 09cea260e..08e97d8f5 100644 --- a/crates/voicevox_core_python_api/src/lib.rs +++ b/crates/voicevox_core_python_api/src/lib.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{marker::PhantomData, sync::Arc}; mod convert; use convert::*; @@ -9,7 +9,7 @@ use pyo3::{ exceptions::PyException, pyclass, pyfunction, pymethods, pymodule, types::{IntoPyDict as _, PyBytes, PyDict, PyList, PyModule}, - wrap_pyfunction, PyAny, PyObject, PyResult, Python, ToPyObject, + wrap_pyfunction, PyAny, PyObject, PyRef, PyResult, PyTypeInfo, Python, ToPyObject, }; use tokio::{runtime::Runtime, sync::Mutex}; use uuid::Uuid; @@ -114,7 +114,7 @@ impl OpenJtalk { #[pyclass] struct Synthesizer { - synthesizer: Arc>, + synthesizer: Closable>, Self>, } #[pymethods] @@ -143,9 +143,10 @@ impl Synthesizer { }, ) .await - .into_py_result()?; + .into_py_result()? + .into(); Ok(Self { - synthesizer: Arc::new(Mutex::new(synthesizer)), + synthesizer: Closable::new(Arc::new(synthesizer)), }) }) } @@ -154,14 +155,29 @@ impl Synthesizer { "Synthesizer { .. }" } + fn __enter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + + fn __exit__( + &mut self, + #[allow(unused_variables)] exc_type: &PyAny, + #[allow(unused_variables)] exc_value: &PyAny, + #[allow(unused_variables)] traceback: &PyAny, + ) -> PyResult<()> { + self.close() + } + #[getter] - fn is_gpu_mode(&self) -> bool { - RUNTIME.block_on(self.synthesizer.lock()).is_gpu_mode() + fn is_gpu_mode(&self) -> PyResult { + let synthesizer = self.synthesizer.get()?; + Ok(RUNTIME.block_on(synthesizer.lock()).is_gpu_mode()) } #[getter] - fn metas<'py>(&self, py: Python<'py>) -> Vec<&'py PyAny> { - to_pydantic_voice_model_meta(RUNTIME.block_on(self.synthesizer.lock()).metas(), py).unwrap() + fn metas<'py>(&self, py: Python<'py>) -> PyResult> { + let synthesizer = self.synthesizer.get()?; + to_pydantic_voice_model_meta(RUNTIME.block_on(synthesizer.lock()).metas(), py) } fn load_voice_model<'py>( @@ -170,7 +186,7 @@ impl Synthesizer { py: Python<'py>, ) -> PyResult<&'py PyAny> { let model: VoiceModel = model.extract()?; - let synthesizer = self.synthesizer.clone(); + let synthesizer = self.synthesizer.get()?.clone(); pyo3_asyncio::tokio::future_into_py(py, async move { synthesizer .lock() @@ -183,15 +199,15 @@ impl Synthesizer { fn unload_voice_model(&mut self, voice_model_id: &str) -> PyResult<()> { RUNTIME - .block_on(self.synthesizer.lock()) + .block_on(self.synthesizer.get()?.lock()) .unload_voice_model(&VoiceModelId::new(voice_model_id.to_string())) .into_py_result() } - fn is_loaded_voice_model(&self, voice_model_id: &str) -> bool { - RUNTIME - .block_on(self.synthesizer.lock()) - .is_loaded_voice_model(&VoiceModelId::new(voice_model_id.to_string())) + fn is_loaded_voice_model(&self, voice_model_id: &str) -> PyResult { + Ok(RUNTIME + .block_on(self.synthesizer.get()?.lock()) + .is_loaded_voice_model(&VoiceModelId::new(voice_model_id.to_string()))) } #[pyo3(signature=(text,style_id,kana = AudioQueryOptions::default().kana))] @@ -202,7 +218,7 @@ impl Synthesizer { kana: bool, py: Python<'py>, ) -> PyResult<&'py PyAny> { - let synthesizer = self.synthesizer.clone(); + let synthesizer = self.synthesizer.get()?.clone(); let text = text.to_owned(); pyo3_asyncio::tokio::future_into_py_with_locals( py, @@ -232,7 +248,7 @@ impl Synthesizer { kana: bool, py: Python<'py>, ) -> PyResult<&'py PyAny> { - let synthesizer = self.synthesizer.clone(); + let synthesizer = self.synthesizer.get()?.clone(); let text = text.to_owned(); pyo3_asyncio::tokio::future_into_py_with_locals( py, @@ -267,7 +283,7 @@ impl Synthesizer { style_id: u32, py: Python<'py>, ) -> PyResult<&'py PyAny> { - let synthesizer = self.synthesizer.clone(); + let synthesizer = self.synthesizer.get()?.clone(); modify_accent_phrases( accent_phrases, StyleId::new(style_id), @@ -282,7 +298,7 @@ impl Synthesizer { style_id: u32, py: Python<'py>, ) -> PyResult<&'py PyAny> { - let synthesizer = self.synthesizer.clone(); + let synthesizer = self.synthesizer.get()?.clone(); modify_accent_phrases( accent_phrases, StyleId::new(style_id), @@ -297,7 +313,7 @@ impl Synthesizer { style_id: u32, py: Python<'py>, ) -> PyResult<&'py PyAny> { - let synthesizer = self.synthesizer.clone(); + let synthesizer = self.synthesizer.get()?.clone(); modify_accent_phrases( accent_phrases, StyleId::new(style_id), @@ -314,7 +330,7 @@ impl Synthesizer { enable_interrogative_upspeak: bool, py: Python<'py>, ) -> PyResult<&'py PyAny> { - let synthesizer = self.synthesizer.clone(); + let synthesizer = self.synthesizer.get()?.clone(); pyo3_asyncio::tokio::future_into_py_with_locals( py, pyo3_asyncio::tokio::get_current_locals(py)?, @@ -355,7 +371,7 @@ impl Synthesizer { kana, enable_interrogative_upspeak, }; - let synthesizer = self.synthesizer.clone(); + let synthesizer = self.synthesizer.get()?.clone(); let text = text.to_owned(); pyo3_asyncio::tokio::future_into_py_with_locals( py, @@ -371,6 +387,52 @@ impl Synthesizer { }, ) } + + fn close(&mut self) -> PyResult<()> { + self.synthesizer.close() + } +} + +struct Closable { + content: MaybeClosed, + marker: PhantomData, +} + +enum MaybeClosed { + Open(T), + Closed, +} + +impl Closable { + fn new(content: T) -> Self { + Self { + content: MaybeClosed::Open(content), + marker: PhantomData, + } + } + + fn get(&self) -> PyResult<&T> { + match &self.content { + MaybeClosed::Open(content) => Ok(content), + MaybeClosed::Closed => Err(VoicevoxError::new_err(format!( + "The `{}` is already closed", + C::NAME, + ))), + } + } + + fn close(&mut self) -> PyResult<()> { + self.get()?; + debug!("Closing a {}", C::NAME); + self.content = MaybeClosed::Closed; + Ok(()) + } +} + +impl Drop for Closable { + fn drop(&mut self) { + let _ = self.close(); + } } #[pyfunction] @@ -451,9 +513,3 @@ impl UserDict { Ok(words.into_py_dict(py)) } } - -impl Drop for Synthesizer { - fn drop(&mut self) { - debug!("Destructing a VoicevoxCore"); - } -} From 10c468b1004937b3938aed2f4d5a6417202cc9d2 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Thu, 10 Aug 2023 01:33:26 +0900 Subject: [PATCH 2/8] =?UTF-8?q?`IOBase`=E3=81=AE=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AA=E6=8C=99=E5=8B=95=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/voicevox_core_python_api/src/lib.rs | 23 +++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/crates/voicevox_core_python_api/src/lib.rs b/crates/voicevox_core_python_api/src/lib.rs index 08e97d8f5..40b9e31bd 100644 --- a/crates/voicevox_core_python_api/src/lib.rs +++ b/crates/voicevox_core_python_api/src/lib.rs @@ -155,8 +155,9 @@ impl Synthesizer { "Synthesizer { .. }" } - fn __enter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { - slf + fn __enter__(slf: PyRef<'_, Self>) -> PyResult> { + slf.synthesizer.get()?; + Ok(slf) } fn __exit__( @@ -164,8 +165,8 @@ impl Synthesizer { #[allow(unused_variables)] exc_type: &PyAny, #[allow(unused_variables)] exc_value: &PyAny, #[allow(unused_variables)] traceback: &PyAny, - ) -> PyResult<()> { - self.close() + ) { + self.close(); } #[getter] @@ -388,7 +389,7 @@ impl Synthesizer { ) } - fn close(&mut self) -> PyResult<()> { + fn close(&mut self) { self.synthesizer.close() } } @@ -415,23 +416,23 @@ impl Closable { match &self.content { MaybeClosed::Open(content) => Ok(content), MaybeClosed::Closed => Err(VoicevoxError::new_err(format!( - "The `{}` is already closed", + "The `{}` is closed", C::NAME, ))), } } - fn close(&mut self) -> PyResult<()> { - self.get()?; - debug!("Closing a {}", C::NAME); + fn close(&mut self) { + if matches!(self.content, MaybeClosed::Open(_)) { + debug!("Closing a {}", C::NAME); + } self.content = MaybeClosed::Closed; - Ok(()) } } impl Drop for Closable { fn drop(&mut self) { - let _ = self.close(); + self.close(); } } From 068f6354db5615eb37445d15060d63ad69daddc4 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Thu, 10 Aug 2023 01:34:36 +0900 Subject: [PATCH 3/8] =?UTF-8?q?`=5F=5Fexit=5F=5F`=E3=81=AE=E6=88=BB?= =?UTF-8?q?=E3=82=8A=E5=80=A4=E3=82=92=E5=9E=8B=E4=BB=98=E3=81=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi b/crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi index 8e9065cdc..66625d230 100644 --- a/crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi +++ b/crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi @@ -91,7 +91,7 @@ class Synthesizer: ... def __repr__(self) -> str: ... def __enter__(self) -> "Synthesizer": ... - def __exit__(self, exc_type, exc_value, traceback): ... + def __exit__(self, exc_type, exc_value, traceback) -> None: ... @property def is_gpu_mode(self) -> bool: """ハードウェアアクセラレーションがGPUモードかどうか。""" From 1e5f5904196e43be09ce4a71c53fff320b4a471d Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Sun, 13 Aug 2023 00:29:22 +0900 Subject: [PATCH 4/8] =?UTF-8?q?test=5Fpseudo=5Fraii=5Ffor=5Fsynthesizer.py?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/test_pseudo_raii_for_synthesizer.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py diff --git a/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py b/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py new file mode 100644 index 000000000..48a428a86 --- /dev/null +++ b/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py @@ -0,0 +1,31 @@ +import conftest +import pytest +from voicevox_core import OpenJtalk, Synthesizer, VoicevoxError + + +@pytest.mark.asyncio +async def test_enter() -> None: + open_jtalk = OpenJtalk(conftest.open_jtalk_dic_dir) + + with await Synthesizer.new_with_initialize(open_jtalk) as synthesizer: + synthesizer.metas + + +@pytest.mark.asyncio +async def test_access_after_close_denied() -> None: + open_jtalk = OpenJtalk(conftest.open_jtalk_dic_dir) + + synthesizer = await Synthesizer.new_with_initialize(open_jtalk) + synthesizer.close() + with pytest.raises(VoicevoxError, match="^The `Synthesizer` is closed$"): + synthesizer.metas + + +@pytest.mark.asyncio +async def test_access_after_exit_denied() -> None: + open_jtalk = OpenJtalk(conftest.open_jtalk_dic_dir) + + with await Synthesizer.new_with_initialize(open_jtalk) as synthesizer: + pass + with pytest.raises(VoicevoxError, match="^The `Synthesizer` is closed$"): + synthesizer.metas From 244acb6915480b93f9aad4571cdf241fac97174f Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Sun, 13 Aug 2023 00:38:08 +0900 Subject: [PATCH 5/8] =?UTF-8?q?test=5Fpseudo=5Fraii=5Ffor=5Fsynthesizer.py?= =?UTF-8?q?=E3=82=92=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/test_pseudo_raii_for_synthesizer.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py b/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py index 48a428a86..ad0fec7ee 100644 --- a/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py +++ b/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py @@ -4,13 +4,25 @@ @pytest.mark.asyncio -async def test_enter() -> None: +async def test_enter_returns_workable_self() -> None: open_jtalk = OpenJtalk(conftest.open_jtalk_dic_dir) with await Synthesizer.new_with_initialize(open_jtalk) as synthesizer: synthesizer.metas +@pytest.mark.asyncio +async def test_closing_multiple_times_is_allowed() -> None: + open_jtalk = OpenJtalk(conftest.open_jtalk_dic_dir) + + with await Synthesizer.new_with_initialize(open_jtalk) as synthesizer: + with synthesizer: + pass + + synthesizer.close() + synthesizer.close() + + @pytest.mark.asyncio async def test_access_after_close_denied() -> None: open_jtalk = OpenJtalk(conftest.open_jtalk_dic_dir) From 47f5388b5191bf2c0ad2db02794a95452fd95946 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Sun, 13 Aug 2023 00:52:42 +0900 Subject: [PATCH 6/8] =?UTF-8?q?`OpenJtalk`=E3=82=92fixture=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/test_pseudo_raii_for_synthesizer.py | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py b/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py index ad0fec7ee..06b37d2f7 100644 --- a/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py +++ b/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py @@ -4,17 +4,13 @@ @pytest.mark.asyncio -async def test_enter_returns_workable_self() -> None: - open_jtalk = OpenJtalk(conftest.open_jtalk_dic_dir) - +async def test_enter_returns_workable_self(open_jtalk: OpenJtalk) -> None: with await Synthesizer.new_with_initialize(open_jtalk) as synthesizer: synthesizer.metas @pytest.mark.asyncio -async def test_closing_multiple_times_is_allowed() -> None: - open_jtalk = OpenJtalk(conftest.open_jtalk_dic_dir) - +async def test_closing_multiple_times_is_allowed(open_jtalk: OpenJtalk) -> None: with await Synthesizer.new_with_initialize(open_jtalk) as synthesizer: with synthesizer: pass @@ -24,9 +20,7 @@ async def test_closing_multiple_times_is_allowed() -> None: @pytest.mark.asyncio -async def test_access_after_close_denied() -> None: - open_jtalk = OpenJtalk(conftest.open_jtalk_dic_dir) - +async def test_access_after_close_denied(open_jtalk: OpenJtalk) -> None: synthesizer = await Synthesizer.new_with_initialize(open_jtalk) synthesizer.close() with pytest.raises(VoicevoxError, match="^The `Synthesizer` is closed$"): @@ -34,10 +28,13 @@ async def test_access_after_close_denied() -> None: @pytest.mark.asyncio -async def test_access_after_exit_denied() -> None: - open_jtalk = OpenJtalk(conftest.open_jtalk_dic_dir) - +async def test_access_after_exit_denied(open_jtalk: OpenJtalk) -> None: with await Synthesizer.new_with_initialize(open_jtalk) as synthesizer: pass with pytest.raises(VoicevoxError, match="^The `Synthesizer` is closed$"): synthesizer.metas + + +@pytest.fixture(scope="module") +def open_jtalk() -> OpenJtalk: + return OpenJtalk(conftest.open_jtalk_dic_dir) From 2f557f35a763dfda7d4608aa0f45a2784e2d621f Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Sun, 13 Aug 2023 10:26:04 +0900 Subject: [PATCH 7/8] =?UTF-8?q?`Synthesizer`=E3=82=82fixture=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/test_pseudo_raii_for_synthesizer.py | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py b/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py index 06b37d2f7..b934b3b12 100644 --- a/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py +++ b/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py @@ -1,38 +1,39 @@ import conftest import pytest +import pytest_asyncio from voicevox_core import OpenJtalk, Synthesizer, VoicevoxError -@pytest.mark.asyncio -async def test_enter_returns_workable_self(open_jtalk: OpenJtalk) -> None: - with await Synthesizer.new_with_initialize(open_jtalk) as synthesizer: - synthesizer.metas +def test_enter_returns_workable_self(synthesizer: Synthesizer) -> None: + with synthesizer as ctx: + assert ctx is synthesizer + _ = synthesizer.metas -@pytest.mark.asyncio -async def test_closing_multiple_times_is_allowed(open_jtalk: OpenJtalk) -> None: - with await Synthesizer.new_with_initialize(open_jtalk) as synthesizer: +def test_closing_multiple_times_is_allowed(synthesizer: Synthesizer) -> None: + with synthesizer: with synthesizer: pass - synthesizer.close() synthesizer.close() -@pytest.mark.asyncio -async def test_access_after_close_denied(open_jtalk: OpenJtalk) -> None: - synthesizer = await Synthesizer.new_with_initialize(open_jtalk) +def test_access_after_close_denied(synthesizer: Synthesizer) -> None: synthesizer.close() with pytest.raises(VoicevoxError, match="^The `Synthesizer` is closed$"): - synthesizer.metas + _ = synthesizer.metas -@pytest.mark.asyncio -async def test_access_after_exit_denied(open_jtalk: OpenJtalk) -> None: - with await Synthesizer.new_with_initialize(open_jtalk) as synthesizer: +def test_access_after_exit_denied(synthesizer: Synthesizer) -> None: + with synthesizer: pass with pytest.raises(VoicevoxError, match="^The `Synthesizer` is closed$"): - synthesizer.metas + _ = synthesizer.metas + + +@pytest_asyncio.fixture +async def synthesizer(open_jtalk: OpenJtalk) -> Synthesizer: + return await Synthesizer.new_with_initialize(open_jtalk) @pytest.fixture(scope="module") From af52951ac1d1a9b95a629298e74b9ca4e1d61ac5 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Tue, 15 Aug 2023 12:25:56 +0900 Subject: [PATCH 8/8] =?UTF-8?q?`test=5Fpseudo=5Fraii=5Ffor=5Fsynthesizer`?= =?UTF-8?q?=E3=81=ABdoc=E3=82=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../python/test/test_pseudo_raii_for_synthesizer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py b/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py index b934b3b12..165770dab 100644 --- a/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py +++ b/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py @@ -1,3 +1,7 @@ +""" +``Synthesizer`` について、(広義の)RAIIができることをテストする。 +""" + import conftest import pytest import pytest_asyncio