From b9edd5be611b431891414c14f7c6e372c370efb8 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Wed, 6 Dec 2023 00:57:50 +0900 Subject: [PATCH 01/13] `blocking.{OpenJtalk,UserDict}` --- .github/workflows/generate_document.yml | 3 + .../python/voicevox_core/__init__.py | 3 + .../{_rust.pyi => _rust/__init__.pyi} | 0 .../python/voicevox_core/_rust/blocking.pyi | 104 ++++++++++++++ .../python/voicevox_core/blocking.py | 3 + .../voicevox_core_python_api/src/convert.rs | 2 +- crates/voicevox_core_python_api/src/lib.rs | 136 ++++++++++++++++-- docs/apis/python_api/conf.py | 1 + 8 files changed, 243 insertions(+), 9 deletions(-) rename crates/voicevox_core_python_api/python/voicevox_core/{_rust.pyi => _rust/__init__.pyi} (100%) create mode 100644 crates/voicevox_core_python_api/python/voicevox_core/_rust/blocking.pyi create mode 100644 crates/voicevox_core_python_api/python/voicevox_core/blocking.py diff --git a/.github/workflows/generate_document.yml b/.github/workflows/generate_document.yml index 9a2328cfc..e3890d82a 100644 --- a/.github/workflows/generate_document.yml +++ b/.github/workflows/generate_document.yml @@ -49,6 +49,9 @@ jobs: run: | cargo build -p voicevox_core_c_api -vv maturin develop --manifest-path ./crates/voicevox_core_python_api/Cargo.toml --locked + # https://github.com/readthedocs/sphinx-autoapi/issues/405 + - name: Workaround to make Sphinx recognize `_rust` as a module + run: touch ./crates/voicevox_core_python_api/python/voicevox_core/_rust/__init__.py - name: Generate Sphinx document run: sphinx-build docs/apis/python_api public/apis/python_api - name: Generate Javadoc diff --git a/crates/voicevox_core_python_api/python/voicevox_core/__init__.py b/crates/voicevox_core_python_api/python/voicevox_core/__init__.py index fc09808bd..053a7bf1c 100644 --- a/crates/voicevox_core_python_api/python/voicevox_core/__init__.py +++ b/crates/voicevox_core_python_api/python/voicevox_core/__init__.py @@ -41,11 +41,14 @@ supported_devices, ) +from . import blocking # noqa: F401 isort: skip + __all__ = [ "__version__", "AccelerationMode", "AccentPhrase", "AudioQuery", + "blocking", "ExtractFullContextLabelError", "GetSupportedDevicesError", "GpuSupportError", diff --git a/crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi b/crates/voicevox_core_python_api/python/voicevox_core/_rust/__init__.pyi similarity index 100% rename from crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi rename to crates/voicevox_core_python_api/python/voicevox_core/_rust/__init__.pyi 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 new file mode 100644 index 000000000..89ebcb0f9 --- /dev/null +++ b/crates/voicevox_core_python_api/python/voicevox_core/_rust/blocking.pyi @@ -0,0 +1,104 @@ +from pathlib import Path +from typing import TYPE_CHECKING, Dict, Union +from uuid import UUID + +if TYPE_CHECKING: + from voicevox_core import UserDictWord + +class OpenJtalk: + """ + テキスト解析器としてのOpen JTalk。 + + Parameters + ---------- + open_jtalk_dict_dir + Open JTalkの辞書ディレクトリ。 + """ + + def __init__(self, open_jtalk_dict_dir: Union[Path, str]) -> None: ... + def use_user_dict(self, user_dict: UserDict) -> None: + """ + ユーザー辞書を設定する。 + + この関数を呼び出した後にユーザー辞書を変更した場合は、再度この関数を呼ぶ必要がある。 + + Parameters + ---------- + user_dict + ユーザー辞書。 + """ + ... + +class UserDict: + """ユーザー辞書。""" + + @property + def words(self) -> Dict[UUID, UserDictWord]: + """このオプジェクトの :class:`dict` としての表現。""" + ... + def __init__(self) -> None: ... + def load(self, path: str) -> None: + """ファイルに保存されたユーザー辞書を読み込む。 + + Parameters + ---------- + path + ユーザー辞書のパス。 + """ + ... + def save(self, path: str) -> None: + """ + ユーザー辞書をファイルに保存する。 + + Parameters + ---------- + path + ユーザー辞書のパス。 + """ + ... + def add_word(self, word: UserDictWord) -> UUID: + """ + 単語を追加する。 + + Parameters + ---------- + word + 追加する単語。 + + Returns + ------- + 単語のUUID。 + """ + ... + def update_word(self, word_uuid: UUID, word: UserDictWord) -> None: + """ + 単語を更新する。 + + Parameters + ---------- + word_uuid + 更新する単語のUUID。 + word + 新しい単語のデータ。 + """ + ... + def remove_word(self, word_uuid: UUID) -> None: + """ + 単語を削除する。 + + Parameters + ---------- + word_uuid + 削除する単語のUUID。 + """ + ... + def import_dict(self, other: UserDict) -> None: + """ + ユーザー辞書をインポートする。 + + Parameters + ---------- + other + インポートするユーザー辞書。 + """ + ... diff --git a/crates/voicevox_core_python_api/python/voicevox_core/blocking.py b/crates/voicevox_core_python_api/python/voicevox_core/blocking.py new file mode 100644 index 000000000..531c9b3d3 --- /dev/null +++ b/crates/voicevox_core_python_api/python/voicevox_core/blocking.py @@ -0,0 +1,3 @@ +from ._rust.blocking import OpenJtalk, UserDict + +__all__ = ["OpenJtalk", "UserDict"] diff --git a/crates/voicevox_core_python_api/src/convert.rs b/crates/voicevox_core_python_api/src/convert.rs index 7e8d437ad..36d12f20a 100644 --- a/crates/voicevox_core_python_api/src/convert.rs +++ b/crates/voicevox_core_python_api/src/convert.rs @@ -150,7 +150,7 @@ pub fn to_rust_word_type(word_type: &PyAny) -> PyResult { serde_json::from_value::(json!(name)).into_py_value_result() } -#[ext] +#[ext(VoicevoxCoreResultExt)] pub impl voicevox_core::Result { fn into_py_result(self, py: Python<'_>) -> PyResult { use voicevox_core::ErrorKind; diff --git a/crates/voicevox_core_python_api/src/lib.rs b/crates/voicevox_core_python_api/src/lib.rs index e841afda1..b1441881f 100644 --- a/crates/voicevox_core_python_api/src/lib.rs +++ b/crates/voicevox_core_python_api/src/lib.rs @@ -6,7 +6,7 @@ use log::debug; use pyo3::{ create_exception, exceptions::{PyException, PyKeyError, PyValueError}, - pyclass, pyfunction, pymethods, pymodule, + py_run, pyclass, pyfunction, pymethods, pymodule, types::{IntoPyDict as _, PyBytes, PyDict, PyList, PyModule}, wrap_pyfunction, PyAny, PyObject, PyRef, PyResult, PyTypeInfo, Python, ToPyObject, }; @@ -18,7 +18,7 @@ use voicevox_core::{ #[pymodule] #[pyo3(name = "_rust")] -fn rust(_: Python<'_>, module: &PyModule) -> PyResult<()> { +fn rust(py: Python<'_>, module: &PyModule) -> PyResult<()> { pyo3_log::init(); module.add("__version__", env!("CARGO_PKG_VERSION"))?; @@ -26,12 +26,23 @@ fn rust(_: Python<'_>, module: &PyModule) -> PyResult<()> { module.add_wrapped(wrap_pyfunction!(_validate_pronunciation))?; module.add_wrapped(wrap_pyfunction!(_to_zenkaku))?; - module.add_class::()?; - module.add_class::()?; - module.add_class::()?; - module.add_class::()?; - - add_exceptions(module) + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; + + add_exceptions(module)?; + + let blocking_module = PyModule::new(py, "voicevox_core._rust.blocking")?; + blocking_module.add_class::()?; + blocking_module.add_class::()?; + // https://github.com/PyO3/pyo3/issues/1517#issuecomment-808664021 + py_run!( + py, + blocking_module, + "import sys; sys.modules['voicevox_core._rust.blocking'] = blocking_module" + ); + module.add_submodule(blocking_module) } macro_rules! exceptions { @@ -609,3 +620,112 @@ impl UserDict { Ok(words.into_py_dict(py)) } } + +mod blocking { + use std::sync::Arc; + + use pyo3::{ + pyclass, pymethods, + types::{IntoPyDict as _, PyDict}, + PyObject, PyResult, Python, + }; + use uuid::Uuid; + use voicevox_core::UserDictWord; + + use crate::convert::VoicevoxCoreResultExt as _; + + #[pyclass] + #[derive(Clone)] + pub(crate) struct OpenJtalk { + open_jtalk: voicevox_core::blocking::OpenJtalk, + } + + #[pymethods] + impl OpenJtalk { + #[new] + fn new( + #[pyo3(from_py_with = "super::from_utf8_path")] open_jtalk_dict_dir: String, + py: Python<'_>, + ) -> PyResult { + let open_jtalk = + voicevox_core::blocking::OpenJtalk::new(open_jtalk_dict_dir).into_py_result(py)?; + Ok(Self { open_jtalk }) + } + + fn use_user_dict(&self, user_dict: UserDict, py: Python<'_>) -> PyResult<()> { + self.open_jtalk + .use_user_dict(&user_dict.dict) + .into_py_result(py) + } + } + + #[pyclass] + #[derive(Default, Debug, Clone)] + pub(crate) struct UserDict { + dict: Arc, + } + + #[pymethods] + impl UserDict { + #[new] + fn new() -> Self { + Self::default() + } + + fn load(&self, path: &str, py: Python<'_>) -> PyResult<()> { + self.dict.load(path).into_py_result(py) + } + + fn save(&self, path: &str, py: Python<'_>) -> PyResult<()> { + self.dict.save(path).into_py_result(py) + } + + fn add_word( + &mut self, + #[pyo3(from_py_with = "crate::convert::to_rust_user_dict_word")] word: UserDictWord, + py: Python<'_>, + ) -> PyResult { + let uuid = self.dict.add_word(word).into_py_result(py)?; + + crate::convert::to_py_uuid(py, uuid) + } + + fn update_word( + &mut self, + #[pyo3(from_py_with = "crate::convert::to_rust_uuid")] word_uuid: Uuid, + #[pyo3(from_py_with = "crate::convert::to_rust_user_dict_word")] word: UserDictWord, + py: Python<'_>, + ) -> PyResult<()> { + self.dict.update_word(word_uuid, word).into_py_result(py) + } + + fn remove_word( + &mut self, + #[pyo3(from_py_with = "crate::convert::to_rust_uuid")] word_uuid: Uuid, + py: Python<'_>, + ) -> PyResult<()> { + self.dict.remove_word(word_uuid).into_py_result(py)?; + Ok(()) + } + + fn import_dict(&mut self, other: &UserDict, py: Python<'_>) -> PyResult<()> { + self.dict.import(&other.dict).into_py_result(py)?; + Ok(()) + } + + #[getter] + fn words<'py>(&self, py: Python<'py>) -> PyResult<&'py PyDict> { + let words = self.dict.with_words(|words| { + words + .iter() + .map(|(&uuid, word)| { + let uuid = crate::convert::to_py_uuid(py, uuid)?; + let word = crate::convert::to_py_user_dict_word(py, word)?; + Ok((uuid, word)) + }) + .collect::>>() + })?; + Ok(words.into_py_dict(py)) + } + } +} diff --git a/docs/apis/python_api/conf.py b/docs/apis/python_api/conf.py index 39f7f8177..b1695b750 100644 --- a/docs/apis/python_api/conf.py +++ b/docs/apis/python_api/conf.py @@ -20,6 +20,7 @@ autoapi_type = "python" autoapi_dirs = ["../../../crates/voicevox_core_python_api/python"] +autoapi_file_patterns = ["*.pyi", "*.py"] autoapi_ignore = ["*test*"] autoapi_options = [ "members", From 4b748f5a7109c38c6487bd2dac2c124225d98c70 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Wed, 6 Dec 2023 02:06:45 +0900 Subject: [PATCH 02/13] `voicevox_core.asyncio` --- .../test/test_pseudo_raii_for_synthesizer.py | 2 +- .../python/test/test_user_dict_load.py | 8 +- .../python/test/test_user_dict_manipulate.py | 6 +- .../python/voicevox_core/__init__.py | 11 +- .../python/voicevox_core/_rust/__init__.pyi | 415 +----------------- .../python/voicevox_core/_rust/asyncio.pyi | 413 +++++++++++++++++ .../python/voicevox_core/asyncio.py | 3 + crates/voicevox_core_python_api/src/lib.rs | 31 +- example/python/run.py | 10 +- 9 files changed, 450 insertions(+), 449 deletions(-) create mode 100644 crates/voicevox_core_python_api/python/voicevox_core/_rust/asyncio.pyi create mode 100644 crates/voicevox_core_python_api/python/voicevox_core/asyncio.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 index a40c9c160..69ff89d2a 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 @@ -5,7 +5,7 @@ import conftest import pytest import pytest_asyncio -from voicevox_core import OpenJtalk, Synthesizer +from voicevox_core.asyncio import OpenJtalk, Synthesizer def test_enter_returns_workable_self(synthesizer: Synthesizer) -> None: diff --git a/crates/voicevox_core_python_api/python/test/test_user_dict_load.py b/crates/voicevox_core_python_api/python/test/test_user_dict_load.py index 572046496..e91773666 100644 --- a/crates/voicevox_core_python_api/python/test/test_user_dict_load.py +++ b/crates/voicevox_core_python_api/python/test/test_user_dict_load.py @@ -10,9 +10,9 @@ @pytest.mark.asyncio async def test_user_dict_load() -> None: - open_jtalk = await voicevox_core.OpenJtalk.new(conftest.open_jtalk_dic_dir) - model = await voicevox_core.VoiceModel.from_path(conftest.model_dir) - synthesizer = voicevox_core.Synthesizer(open_jtalk) + open_jtalk = await voicevox_core.asyncio.OpenJtalk.new(conftest.open_jtalk_dic_dir) + model = await voicevox_core.asyncio.VoiceModel.from_path(conftest.model_dir) + synthesizer = voicevox_core.asyncio.Synthesizer(open_jtalk) await synthesizer.load_voice_model(model) @@ -20,7 +20,7 @@ async def test_user_dict_load() -> None: "this_word_should_not_exist_in_default_dictionary", style_id=0 ) - temp_dict = voicevox_core.UserDict() + temp_dict = voicevox_core.asyncio.UserDict() uuid = temp_dict.add_word( voicevox_core.UserDictWord( surface="this_word_should_not_exist_in_default_dictionary", diff --git a/crates/voicevox_core_python_api/python/test/test_user_dict_manipulate.py b/crates/voicevox_core_python_api/python/test/test_user_dict_manipulate.py index 1ba37465f..652ddfc7a 100644 --- a/crates/voicevox_core_python_api/python/test/test_user_dict_manipulate.py +++ b/crates/voicevox_core_python_api/python/test/test_user_dict_manipulate.py @@ -12,7 +12,7 @@ @pytest.mark.asyncio async def test_user_dict_load() -> None: - dict_a = voicevox_core.UserDict() + dict_a = voicevox_core.asyncio.UserDict() # 単語の追加 uuid_a = dict_a.add_word( @@ -38,7 +38,7 @@ async def test_user_dict_load() -> None: assert dict_a.words[uuid_a].pronunciation == "フガ" # ユーザー辞書のインポート - dict_b = voicevox_core.UserDict() + dict_b = voicevox_core.asyncio.UserDict() uuid_b = dict_b.add_word( voicevox_core.UserDictWord( surface="foo", @@ -50,7 +50,7 @@ async def test_user_dict_load() -> None: assert uuid_b in dict_a.words # ユーザー辞書のエクスポート - dict_c = voicevox_core.UserDict() + dict_c = voicevox_core.asyncio.UserDict() uuid_c = dict_c.add_word( voicevox_core.UserDictWord( surface="bar", diff --git a/crates/voicevox_core_python_api/python/voicevox_core/__init__.py b/crates/voicevox_core_python_api/python/voicevox_core/__init__.py index 053a7bf1c..4ccbad3fe 100644 --- a/crates/voicevox_core_python_api/python/voicevox_core/__init__.py +++ b/crates/voicevox_core_python_api/python/voicevox_core/__init__.py @@ -25,29 +25,26 @@ ModelAlreadyLoadedError, ModelNotFoundError, NotLoadedOpenjtalkDictError, - OpenJtalk, OpenZipFileError, ParseKanaError, ReadZipEntryError, SaveUserDictError, StyleAlreadyLoadedError, StyleNotFoundError, - Synthesizer, - UserDict, UseUserDictError, - VoiceModel, WordNotFoundError, __version__, supported_devices, ) -from . import blocking # noqa: F401 isort: skip +from . import asyncio, blocking # noqa: F401 isort: skip __all__ = [ "__version__", "AccelerationMode", "AccentPhrase", "AudioQuery", + "asyncio", "blocking", "ExtractFullContextLabelError", "GetSupportedDevicesError", @@ -60,7 +57,6 @@ "ModelNotFoundError", "Mora", "NotLoadedOpenjtalkDictError", - "OpenJtalk", "OpenZipFileError", "ParseKanaError", "ReadZipEntryError", @@ -71,11 +67,8 @@ "StyleNotFoundError", "StyleVersion", "SupportedDevices", - "Synthesizer", - "VoiceModel", "supported_devices", "UseUserDictError", - "UserDict", "UserDictWord", "UserDictWordType", "VoiceModelId", diff --git a/crates/voicevox_core_python_api/python/voicevox_core/_rust/__init__.pyi b/crates/voicevox_core_python_api/python/voicevox_core/_rust/__init__.pyi index b09f8425f..3a47ef02b 100644 --- a/crates/voicevox_core_python_api/python/voicevox_core/_rust/__init__.pyi +++ b/crates/voicevox_core_python_api/python/voicevox_core/_rust/__init__.pyi @@ -1,19 +1,7 @@ -from pathlib import Path -from typing import TYPE_CHECKING, Dict, List, Literal, Union -from uuid import UUID +from typing import TYPE_CHECKING if TYPE_CHECKING: - from voicevox_core import ( - AccelerationMode, - AccentPhrase, - AudioQuery, - SpeakerMeta, - StyleId, - SupportedDevices, - UserDict, - UserDictWord, - VoiceModelId, - ) + from voicevox_core import SupportedDevices __version__: str @@ -29,405 +17,6 @@ def supported_devices() -> SupportedDevices: """ ... -class VoiceModel: - """ - 音声モデル。""" - - @staticmethod - async def from_path(path: Union[Path, str]) -> VoiceModel: - """ - VVMファイルから ``VoiceModel`` を生成する。 - - Parameters - ---------- - path - VVMファイルへのパス。 - """ - ... - @property - def id(self) -> VoiceModelId: - """ID。""" - ... - @property - def metas(self) -> List[SpeakerMeta]: - """メタ情報。""" - ... - -class OpenJtalk: - """ - テキスト解析器としてのOpen JTalk。 - """ - - @staticmethod - async def new(open_jtalk_dict_dir: Union[Path, str]) -> "OpenJtalk": - """ - ``OpenJTalk`` を生成する。 - - Parameters - ---------- - open_jtalk_dict_dir - Open JTalkの辞書ディレクトリ。 - """ - ... - async def use_user_dict(self, user_dict: UserDict) -> None: - """ - ユーザー辞書を設定する。 - - この関数を呼び出した後にユーザー辞書を変更した場合は、再度この関数を呼ぶ必要がある。 - - Parameters - ---------- - user_dict - ユーザー辞書。 - """ - ... - -class Synthesizer: - """ - 音声シンセサイザ。 - - Parameters - ---------- - open_jtalk - Open JTalk。 - acceleration_mode - ハードウェアアクセラレーションモード。 - cpu_num_threads - CPU利用数を指定。0を指定すると環境に合わせたCPUが利用される。 - """ - - def __init__( - self, - open_jtalk: OpenJtalk, - acceleration_mode: Union[ - AccelerationMode, Literal["AUTO", "CPU", "GPU"] - ] = AccelerationMode.AUTO, - cpu_num_threads: int = 0, - ) -> None: ... - def __repr__(self) -> str: ... - def __enter__(self) -> "Synthesizer": ... - def __exit__(self, exc_type, exc_value, traceback) -> None: ... - @property - def is_gpu_mode(self) -> bool: - """ハードウェアアクセラレーションがGPUモードかどうか。""" - ... - @property - def metas(self) -> List[SpeakerMeta]: - """メタ情報。""" - ... - async def load_voice_model(self, model: VoiceModel) -> None: - """ - モデルを読み込む。 - - Parameters - ---------- - style_id - 読み込むモデルのスタイルID。 - """ - ... - def unload_voice_model(self, voice_model_id: Union[VoiceModelId, str]) -> None: - """ - 音声モデルの読み込みを解除する。 - - Parameters - ---------- - voice_model_id - 音声モデルID。 - """ - ... - def is_loaded_voice_model(self, voice_model_id: Union[VoiceModelId, str]) -> bool: - """ - 指定したvoice_model_idのモデルが読み込まれているか判定する。 - - Parameters - ---------- - voice_model_id - 音声モデルID。 - - Returns - ------- - モデルが読み込まれているかどうか。 - """ - ... - async def audio_query_from_kana( - self, - kana: str, - style_id: Union[StyleId, int], - ) -> AudioQuery: - """ - AquesTalk風記法から :class:`AudioQuery` を生成する。 - - Parameters - ---------- - kana - AquesTalk風記法。 - style_id - スタイルID。 - - Returns - ------- - 話者とテキストから生成された :class:`AudioQuery` 。 - """ - ... - async def audio_query( - self, - text: str, - style_id: Union[StyleId, int], - ) -> AudioQuery: - """ - 日本語のテキストから :class:`AudioQuery` を生成する。 - - Parameters - ---------- - text - UTF-8の日本語テキスト。 - style_id - スタイルID。 - - Returns - ------- - 話者とテキストから生成された :class:`AudioQuery` 。 - """ - ... - async def create_accent_phrases_from_kana( - self, - kana: str, - style_id: Union[StyleId, int], - ) -> List[AccentPhrase]: - """ - AquesTalk風記法からAccentPhrase(アクセント句)の配列を生成する。 - - Parameters - ---------- - kana - AquesTalk風記法。 - style_id - スタイルID。 - - Returns - ------- - :class:`AccentPhrase` の配列。 - """ - ... - async def create_accent_phrases( - self, - text: str, - style_id: Union[StyleId, int], - ) -> List[AccentPhrase]: - """ - 日本語のテキストからAccentPhrase(アクセント句)の配列を生成する。 - - Parameters - ---------- - text - UTF-8の日本語テキスト。 - style_id - スタイルID。 - - Returns - ------- - :class:`AccentPhrase` の配列。 - """ - ... - async def replace_mora_data( - self, - accent_phrases: List[AccentPhrase], - style_id: Union[StyleId, int], - ) -> List[AccentPhrase]: - """ - アクセント句の音高・音素長を変更した新しいアクセント句の配列を生成する。 - - 元のアクセント句の音高・音素長は変更されない。 - - Parameters - ---------- - accent_phrases: - 変更元のアクセント句。 - style_id: - スタイルID。 - - Returns - ------- - 新しいアクセント句の配列。 - """ - ... - async def replace_phoneme_length( - self, - accent_phrases: List[AccentPhrase], - style_id: Union[StyleId, int], - ) -> List[AccentPhrase]: - """ - アクセント句の音素長を変更した新しいアクセント句の配列を生成する。 - - 元のアクセント句の音素長は変更されない。 - - Parameters - ---------- - accent_phrases - 変更元のアクセント句。 - style_id - スタイルID。 - """ - ... - async def replace_mora_pitch( - self, - accent_phrases: List[AccentPhrase], - style_id: Union[StyleId, int], - ) -> List[AccentPhrase]: - """ - アクセント句の音高を変更した新しいアクセント句の配列を生成する。 - - 元のアクセント句の音高は変更されない。 - - Parameters - ---------- - accent_phrases - 変更元のアクセント句。 - style_id - スタイルID。 - """ - ... - async def synthesis( - self, - audio_query: AudioQuery, - style_id: Union[StyleId, int], - enable_interrogative_upspeak: bool = True, - ) -> bytes: - """ - :class:`AudioQuery` から音声合成する。 - - Parameters - ---------- - audio_query - :class:`AudioQuery` 。 - style_id - スタイルID。 - enable_interrogative_upspeak - 疑問文の調整を有効にするかどうか。 - - Returns - ------- - WAVデータ。 - """ - ... - async def tts_from_kana( - self, - kana: str, - style_id: Union[StyleId, int], - enable_interrogative_upspeak: bool = True, - ) -> bytes: - """ - AquesTalk風記法から音声合成を行う。 - - Parameters - ---------- - kana - AquesTalk風記法。 - style_id - スタイルID。 - enable_interrogative_upspeak - 疑問文の調整を有効にするかどうか。 - """ - ... - async def tts( - self, - text: str, - style_id: Union[StyleId, int], - enable_interrogative_upspeak: bool = True, - ) -> bytes: - """ - 日本語のテキストから音声合成を行う。 - - Parameters - ---------- - text - UTF-8の日本語テキスト。 - style_id - スタイルID。 - enable_interrogative_upspeak - 疑問文の調整を有効にするかどうか。 - - Returns - ------- - WAVデータ。 - """ - ... - def close(self) -> None: ... - -class UserDict: - """ユーザー辞書。""" - - @property - def words(self) -> Dict[UUID, UserDictWord]: - """このオプジェクトの :class:`dict` としての表現。""" - ... - def __init__(self) -> None: ... - async def load(self, path: str) -> None: - """ファイルに保存されたユーザー辞書を読み込む。 - - Parameters - ---------- - path - ユーザー辞書のパス。 - """ - ... - async def save(self, path: str) -> None: - """ - ユーザー辞書をファイルに保存する。 - - Parameters - ---------- - path - ユーザー辞書のパス。 - """ - ... - def add_word(self, word: UserDictWord) -> UUID: - """ - 単語を追加する。 - - Parameters - ---------- - word - 追加する単語。 - - Returns - ------- - 単語のUUID。 - """ - ... - def update_word(self, word_uuid: UUID, word: UserDictWord) -> None: - """ - 単語を更新する。 - - Parameters - ---------- - word_uuid - 更新する単語のUUID。 - word - 新しい単語のデータ。 - """ - ... - def remove_word(self, word_uuid: UUID) -> None: - """ - 単語を削除する。 - - Parameters - ---------- - word_uuid - 削除する単語のUUID。 - """ - ... - def import_dict(self, other: UserDict) -> None: - """ - ユーザー辞書をインポートする。 - - Parameters - ---------- - other - インポートするユーザー辞書。 - """ - ... - class NotLoadedOpenjtalkDictError(Exception): """open_jtalk辞書ファイルが読み込まれていない。""" 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 new file mode 100644 index 000000000..7a6596008 --- /dev/null +++ b/crates/voicevox_core_python_api/python/voicevox_core/_rust/asyncio.pyi @@ -0,0 +1,413 @@ +from pathlib import Path +from typing import TYPE_CHECKING, Dict, List, Literal, Union +from uuid import UUID + +if TYPE_CHECKING: + from voicevox_core import ( + AccelerationMode, + AccentPhrase, + AudioQuery, + SpeakerMeta, + StyleId, + UserDictWord, + VoiceModelId, + ) + +class VoiceModel: + """ + 音声モデル。""" + + @staticmethod + async def from_path(path: Union[Path, str]) -> VoiceModel: + """ + VVMファイルから ``VoiceModel`` を生成する。 + + Parameters + ---------- + path + VVMファイルへのパス。 + """ + ... + @property + def id(self) -> VoiceModelId: + """ID。""" + ... + @property + def metas(self) -> List[SpeakerMeta]: + """メタ情報。""" + ... + +class OpenJtalk: + """ + テキスト解析器としてのOpen JTalk。 + """ + + @staticmethod + async def new(open_jtalk_dict_dir: Union[Path, str]) -> "OpenJtalk": + """ + ``OpenJTalk`` を生成する。 + + Parameters + ---------- + open_jtalk_dict_dir + Open JTalkの辞書ディレクトリ。 + """ + ... + async def use_user_dict(self, user_dict: UserDict) -> None: + """ + ユーザー辞書を設定する。 + + この関数を呼び出した後にユーザー辞書を変更した場合は、再度この関数を呼ぶ必要がある。 + + Parameters + ---------- + user_dict + ユーザー辞書。 + """ + ... + +class Synthesizer: + """ + 音声シンセサイザ。 + + Parameters + ---------- + open_jtalk + Open JTalk。 + acceleration_mode + ハードウェアアクセラレーションモード。 + cpu_num_threads + CPU利用数を指定。0を指定すると環境に合わせたCPUが利用される。 + """ + + def __init__( + self, + open_jtalk: OpenJtalk, + acceleration_mode: Union[ + AccelerationMode, Literal["AUTO", "CPU", "GPU"] + ] = AccelerationMode.AUTO, + cpu_num_threads: int = 0, + ) -> None: ... + def __repr__(self) -> str: ... + def __enter__(self) -> "Synthesizer": ... + def __exit__(self, exc_type, exc_value, traceback) -> None: ... + @property + def is_gpu_mode(self) -> bool: + """ハードウェアアクセラレーションがGPUモードかどうか。""" + ... + @property + def metas(self) -> List[SpeakerMeta]: + """メタ情報。""" + ... + async def load_voice_model(self, model: VoiceModel) -> None: + """ + モデルを読み込む。 + + Parameters + ---------- + style_id + 読み込むモデルのスタイルID。 + """ + ... + def unload_voice_model(self, voice_model_id: Union[VoiceModelId, str]) -> None: + """ + 音声モデルの読み込みを解除する。 + + Parameters + ---------- + voice_model_id + 音声モデルID。 + """ + ... + def is_loaded_voice_model(self, voice_model_id: Union[VoiceModelId, str]) -> bool: + """ + 指定したvoice_model_idのモデルが読み込まれているか判定する。 + + Parameters + ---------- + voice_model_id + 音声モデルID。 + + Returns + ------- + モデルが読み込まれているかどうか。 + """ + ... + async def audio_query_from_kana( + self, + kana: str, + style_id: Union[StyleId, int], + ) -> AudioQuery: + """ + AquesTalk風記法から :class:`AudioQuery` を生成する。 + + Parameters + ---------- + kana + AquesTalk風記法。 + style_id + スタイルID。 + + Returns + ------- + 話者とテキストから生成された :class:`AudioQuery` 。 + """ + ... + async def audio_query( + self, + text: str, + style_id: Union[StyleId, int], + ) -> AudioQuery: + """ + 日本語のテキストから :class:`AudioQuery` を生成する。 + + Parameters + ---------- + text + UTF-8の日本語テキスト。 + style_id + スタイルID。 + + Returns + ------- + 話者とテキストから生成された :class:`AudioQuery` 。 + """ + ... + async def create_accent_phrases_from_kana( + self, + kana: str, + style_id: Union[StyleId, int], + ) -> List[AccentPhrase]: + """ + AquesTalk風記法からAccentPhrase(アクセント句)の配列を生成する。 + + Parameters + ---------- + kana + AquesTalk風記法。 + style_id + スタイルID。 + + Returns + ------- + :class:`AccentPhrase` の配列。 + """ + ... + async def create_accent_phrases( + self, + text: str, + style_id: Union[StyleId, int], + ) -> List[AccentPhrase]: + """ + 日本語のテキストからAccentPhrase(アクセント句)の配列を生成する。 + + Parameters + ---------- + text + UTF-8の日本語テキスト。 + style_id + スタイルID。 + + Returns + ------- + :class:`AccentPhrase` の配列。 + """ + ... + async def replace_mora_data( + self, + accent_phrases: List[AccentPhrase], + style_id: Union[StyleId, int], + ) -> List[AccentPhrase]: + """ + アクセント句の音高・音素長を変更した新しいアクセント句の配列を生成する。 + + 元のアクセント句の音高・音素長は変更されない。 + + Parameters + ---------- + accent_phrases: + 変更元のアクセント句。 + style_id: + スタイルID。 + + Returns + ------- + 新しいアクセント句の配列。 + """ + ... + async def replace_phoneme_length( + self, + accent_phrases: List[AccentPhrase], + style_id: Union[StyleId, int], + ) -> List[AccentPhrase]: + """ + アクセント句の音素長を変更した新しいアクセント句の配列を生成する。 + + 元のアクセント句の音素長は変更されない。 + + Parameters + ---------- + accent_phrases + 変更元のアクセント句。 + style_id + スタイルID。 + """ + ... + async def replace_mora_pitch( + self, + accent_phrases: List[AccentPhrase], + style_id: Union[StyleId, int], + ) -> List[AccentPhrase]: + """ + アクセント句の音高を変更した新しいアクセント句の配列を生成する。 + + 元のアクセント句の音高は変更されない。 + + Parameters + ---------- + accent_phrases + 変更元のアクセント句。 + style_id + スタイルID。 + """ + ... + async def synthesis( + self, + audio_query: AudioQuery, + style_id: Union[StyleId, int], + enable_interrogative_upspeak: bool = True, + ) -> bytes: + """ + :class:`AudioQuery` から音声合成する。 + + Parameters + ---------- + audio_query + :class:`AudioQuery` 。 + style_id + スタイルID。 + enable_interrogative_upspeak + 疑問文の調整を有効にするかどうか。 + + Returns + ------- + WAVデータ。 + """ + ... + async def tts_from_kana( + self, + kana: str, + style_id: Union[StyleId, int], + enable_interrogative_upspeak: bool = True, + ) -> bytes: + """ + AquesTalk風記法から音声合成を行う。 + + Parameters + ---------- + kana + AquesTalk風記法。 + style_id + スタイルID。 + enable_interrogative_upspeak + 疑問文の調整を有効にするかどうか。 + """ + ... + async def tts( + self, + text: str, + style_id: Union[StyleId, int], + enable_interrogative_upspeak: bool = True, + ) -> bytes: + """ + 日本語のテキストから音声合成を行う。 + + Parameters + ---------- + text + UTF-8の日本語テキスト。 + style_id + スタイルID。 + enable_interrogative_upspeak + 疑問文の調整を有効にするかどうか。 + + Returns + ------- + WAVデータ。 + """ + ... + def close(self) -> None: ... + +class UserDict: + """ユーザー辞書。""" + + @property + def words(self) -> Dict[UUID, UserDictWord]: + """このオプジェクトの :class:`dict` としての表現。""" + ... + def __init__(self) -> None: ... + async def load(self, path: str) -> None: + """ファイルに保存されたユーザー辞書を読み込む。 + + Parameters + ---------- + path + ユーザー辞書のパス。 + """ + ... + async def save(self, path: str) -> None: + """ + ユーザー辞書をファイルに保存する。 + + Parameters + ---------- + path + ユーザー辞書のパス。 + """ + ... + def add_word(self, word: UserDictWord) -> UUID: + """ + 単語を追加する。 + + Parameters + ---------- + word + 追加する単語。 + + Returns + ------- + 単語のUUID。 + """ + ... + def update_word(self, word_uuid: UUID, word: UserDictWord) -> None: + """ + 単語を更新する。 + + Parameters + ---------- + word_uuid + 更新する単語のUUID。 + word + 新しい単語のデータ。 + """ + ... + def remove_word(self, word_uuid: UUID) -> None: + """ + 単語を削除する。 + + Parameters + ---------- + word_uuid + 削除する単語のUUID。 + """ + ... + def import_dict(self, other: UserDict) -> None: + """ + ユーザー辞書をインポートする。 + + Parameters + ---------- + other + インポートするユーザー辞書。 + """ + ... diff --git a/crates/voicevox_core_python_api/python/voicevox_core/asyncio.py b/crates/voicevox_core_python_api/python/voicevox_core/asyncio.py new file mode 100644 index 000000000..fec0c831b --- /dev/null +++ b/crates/voicevox_core_python_api/python/voicevox_core/asyncio.py @@ -0,0 +1,3 @@ +from ._rust.asyncio import OpenJtalk, Synthesizer, UserDict, VoiceModel + +__all__ = ["OpenJtalk", "Synthesizer", "UserDict", "VoiceModel"] diff --git a/crates/voicevox_core_python_api/src/lib.rs b/crates/voicevox_core_python_api/src/lib.rs index b1441881f..54a78a2d8 100644 --- a/crates/voicevox_core_python_api/src/lib.rs +++ b/crates/voicevox_core_python_api/src/lib.rs @@ -2,11 +2,12 @@ use std::{marker::PhantomData, sync::Arc}; mod convert; use convert::*; +use easy_ext::ext; use log::debug; use pyo3::{ create_exception, exceptions::{PyException, PyKeyError, PyValueError}, - py_run, pyclass, pyfunction, pymethods, pymodule, + pyclass, pyfunction, pymethods, pymodule, types::{IntoPyDict as _, PyBytes, PyDict, PyList, PyModule}, wrap_pyfunction, PyAny, PyObject, PyRef, PyResult, PyTypeInfo, Python, ToPyObject, }; @@ -26,23 +27,29 @@ fn rust(py: Python<'_>, module: &PyModule) -> PyResult<()> { module.add_wrapped(wrap_pyfunction!(_validate_pronunciation))?; module.add_wrapped(wrap_pyfunction!(_to_zenkaku))?; - module.add_class::()?; - module.add_class::()?; - module.add_class::()?; - module.add_class::()?; - add_exceptions(module)?; let blocking_module = PyModule::new(py, "voicevox_core._rust.blocking")?; blocking_module.add_class::()?; blocking_module.add_class::()?; + module.add_and_register_submodule(blocking_module)?; + + let asyncio_module = PyModule::new(py, "voicevox_core._rust.asyncio")?; + asyncio_module.add_class::()?; + asyncio_module.add_class::()?; + asyncio_module.add_class::()?; + asyncio_module.add_class::()?; + module.add_and_register_submodule(asyncio_module) +} + +#[ext] +impl PyModule { // https://github.com/PyO3/pyo3/issues/1517#issuecomment-808664021 - py_run!( - py, - blocking_module, - "import sys; sys.modules['voicevox_core._rust.blocking'] = blocking_module" - ); - module.add_submodule(blocking_module) + fn add_and_register_submodule(&self, module: &PyModule) -> PyResult<()> { + let sys = self.py().import("sys")?; + sys.getattr("modules")?.set_item(module.name()?, module)?; + self.add_submodule(module) + } } macro_rules! exceptions { diff --git a/example/python/run.py b/example/python/run.py index fb39715e9..7a40f216b 100644 --- a/example/python/run.py +++ b/example/python/run.py @@ -6,14 +6,10 @@ from pathlib import Path from typing import Tuple +from voicevox_core.asyncio import OpenJtalk, Synthesizer, VoiceModel + import voicevox_core -from voicevox_core import ( - AccelerationMode, - AudioQuery, - OpenJtalk, - Synthesizer, - VoiceModel, -) +from voicevox_core import AccelerationMode, AudioQuery # asyncやawaitは必須です。 From 8d0ead3ebf9ac79194ee876138c107a622304538 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Wed, 6 Dec 2023 02:37:58 +0900 Subject: [PATCH 03/13] =?UTF-8?q?Black=E3=81=A8isort=E3=81=AB`--diff`?= =?UTF-8?q?=E3=82=92=E4=BB=98=E3=81=91=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/python_lint.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python_lint.yml b/.github/workflows/python_lint.yml index 4e7c4d8b2..22449c391 100644 --- a/.github/workflows/python_lint.yml +++ b/.github/workflows/python_lint.yml @@ -42,5 +42,5 @@ jobs: - name: Check code style for example/python working-directory: ./example/python run: | - black --check . - isort --check --profile black . + black --check --diff . + isort --check --diff --profile black . From a31760f3cbb77dfce4cc3283e49d24e68953bb08 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Wed, 6 Dec 2023 23:54:59 +0900 Subject: [PATCH 04/13] isort --- example/python/run.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/example/python/run.py b/example/python/run.py index 7a40f216b..28926b127 100644 --- a/example/python/run.py +++ b/example/python/run.py @@ -6,10 +6,9 @@ from pathlib import Path from typing import Tuple -from voicevox_core.asyncio import OpenJtalk, Synthesizer, VoiceModel - import voicevox_core from voicevox_core import AccelerationMode, AudioQuery +from voicevox_core.asyncio import OpenJtalk, Synthesizer, VoiceModel # asyncやawaitは必須です。 From d58ae78996272818857892b3d8ac7774257edddf Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Thu, 7 Dec 2023 00:45:31 +0900 Subject: [PATCH 05/13] `blocking.{Synthesizer,VoiceModel}` --- .../python/voicevox_core/_rust/blocking.pyi | 308 ++++++++++++++++- .../python/voicevox_core/blocking.py | 4 +- .../voicevox_core_python_api/src/convert.rs | 29 +- crates/voicevox_core_python_api/src/lib.rs | 317 +++++++++++++++++- 4 files changed, 646 insertions(+), 12 deletions(-) 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 89ebcb0f9..3a208fb33 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 @@ -1,9 +1,41 @@ from pathlib import Path -from typing import TYPE_CHECKING, Dict, Union +from typing import TYPE_CHECKING, Dict, List, Literal, Union from uuid import UUID if TYPE_CHECKING: - from voicevox_core import UserDictWord + from voicevox_core import ( + AccelerationMode, + AccentPhrase, + AudioQuery, + SpeakerMeta, + StyleId, + UserDictWord, + VoiceModelId, + ) + +class VoiceModel: + """ + 音声モデル。""" + + @staticmethod + def from_path(path: Union[Path, str]) -> VoiceModel: + """ + VVMファイルから ``VoiceModel`` を生成する。 + + Parameters + ---------- + path + VVMファイルへのパス。 + """ + ... + @property + def id(self) -> VoiceModelId: + """ID。""" + ... + @property + def metas(self) -> List[SpeakerMeta]: + """メタ情報。""" + ... class OpenJtalk: """ @@ -29,6 +61,278 @@ class OpenJtalk: """ ... +class Synthesizer: + """ + 音声シンセサイザ。 + + Parameters + ---------- + open_jtalk + Open JTalk。 + acceleration_mode + ハードウェアアクセラレーションモード。 + cpu_num_threads + CPU利用数を指定。0を指定すると環境に合わせたCPUが利用される。 + """ + + def __init__( + self, + open_jtalk: OpenJtalk, + acceleration_mode: Union[ + AccelerationMode, Literal["AUTO", "CPU", "GPU"] + ] = AccelerationMode.AUTO, + cpu_num_threads: int = 0, + ) -> None: ... + def __repr__(self) -> str: ... + def __enter__(self) -> "Synthesizer": ... + def __exit__(self, exc_type, exc_value, traceback) -> None: ... + @property + def is_gpu_mode(self) -> bool: + """ハードウェアアクセラレーションがGPUモードかどうか。""" + ... + @property + def metas(self) -> List[SpeakerMeta]: + """メタ情報。""" + ... + def load_voice_model(self, model: VoiceModel) -> None: + """ + モデルを読み込む。 + + Parameters + ---------- + style_id + 読み込むモデルのスタイルID。 + """ + ... + def unload_voice_model(self, voice_model_id: Union[VoiceModelId, str]) -> None: + """ + 音声モデルの読み込みを解除する。 + + Parameters + ---------- + voice_model_id + 音声モデルID。 + """ + ... + def is_loaded_voice_model(self, voice_model_id: Union[VoiceModelId, str]) -> bool: + """ + 指定したvoice_model_idのモデルが読み込まれているか判定する。 + + Parameters + ---------- + voice_model_id + 音声モデルID。 + + Returns + ------- + モデルが読み込まれているかどうか。 + """ + ... + def audio_query_from_kana( + self, + kana: str, + style_id: Union[StyleId, int], + ) -> AudioQuery: + """ + AquesTalk風記法から :class:`AudioQuery` を生成する。 + + Parameters + ---------- + kana + AquesTalk風記法。 + style_id + スタイルID。 + + Returns + ------- + 話者とテキストから生成された :class:`AudioQuery` 。 + """ + ... + def audio_query( + self, + text: str, + style_id: Union[StyleId, int], + ) -> AudioQuery: + """ + 日本語のテキストから :class:`AudioQuery` を生成する。 + + Parameters + ---------- + text + UTF-8の日本語テキスト。 + style_id + スタイルID。 + + Returns + ------- + 話者とテキストから生成された :class:`AudioQuery` 。 + """ + ... + def create_accent_phrases_from_kana( + self, + kana: str, + style_id: Union[StyleId, int], + ) -> List[AccentPhrase]: + """ + AquesTalk風記法からAccentPhrase(アクセント句)の配列を生成する。 + + Parameters + ---------- + kana + AquesTalk風記法。 + style_id + スタイルID。 + + Returns + ------- + :class:`AccentPhrase` の配列。 + """ + ... + def create_accent_phrases( + self, + text: str, + style_id: Union[StyleId, int], + ) -> List[AccentPhrase]: + """ + 日本語のテキストからAccentPhrase(アクセント句)の配列を生成する。 + + Parameters + ---------- + text + UTF-8の日本語テキスト。 + style_id + スタイルID。 + + Returns + ------- + :class:`AccentPhrase` の配列。 + """ + ... + def replace_mora_data( + self, + accent_phrases: List[AccentPhrase], + style_id: Union[StyleId, int], + ) -> List[AccentPhrase]: + """ + アクセント句の音高・音素長を変更した新しいアクセント句の配列を生成する。 + + 元のアクセント句の音高・音素長は変更されない。 + + Parameters + ---------- + accent_phrases: + 変更元のアクセント句。 + style_id: + スタイルID。 + + Returns + ------- + 新しいアクセント句の配列。 + """ + ... + def replace_phoneme_length( + self, + accent_phrases: List[AccentPhrase], + style_id: Union[StyleId, int], + ) -> List[AccentPhrase]: + """ + アクセント句の音素長を変更した新しいアクセント句の配列を生成する。 + + 元のアクセント句の音素長は変更されない。 + + Parameters + ---------- + accent_phrases + 変更元のアクセント句。 + style_id + スタイルID。 + """ + ... + def replace_mora_pitch( + self, + accent_phrases: List[AccentPhrase], + style_id: Union[StyleId, int], + ) -> List[AccentPhrase]: + """ + アクセント句の音高を変更した新しいアクセント句の配列を生成する。 + + 元のアクセント句の音高は変更されない。 + + Parameters + ---------- + accent_phrases + 変更元のアクセント句。 + style_id + スタイルID。 + """ + ... + def synthesis( + self, + audio_query: AudioQuery, + style_id: Union[StyleId, int], + enable_interrogative_upspeak: bool = True, + ) -> bytes: + """ + :class:`AudioQuery` から音声合成する。 + + Parameters + ---------- + audio_query + :class:`AudioQuery` 。 + style_id + スタイルID。 + enable_interrogative_upspeak + 疑問文の調整を有効にするかどうか。 + + Returns + ------- + WAVデータ。 + """ + ... + def tts_from_kana( + self, + kana: str, + style_id: Union[StyleId, int], + enable_interrogative_upspeak: bool = True, + ) -> bytes: + """ + AquesTalk風記法から音声合成を行う。 + + Parameters + ---------- + kana + AquesTalk風記法。 + style_id + スタイルID。 + enable_interrogative_upspeak + 疑問文の調整を有効にするかどうか。 + """ + ... + def tts( + self, + text: str, + style_id: Union[StyleId, int], + enable_interrogative_upspeak: bool = True, + ) -> bytes: + """ + 日本語のテキストから音声合成を行う。 + + Parameters + ---------- + text + UTF-8の日本語テキスト。 + style_id + スタイルID。 + enable_interrogative_upspeak + 疑問文の調整を有効にするかどうか。 + + Returns + ------- + WAVデータ。 + """ + ... + def close(self) -> None: ... + class UserDict: """ユーザー辞書。""" 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 531c9b3d3..e378037c3 100644 --- a/crates/voicevox_core_python_api/python/voicevox_core/blocking.py +++ b/crates/voicevox_core_python_api/python/voicevox_core/blocking.py @@ -1,3 +1,3 @@ -from ._rust.blocking import OpenJtalk, UserDict +from ._rust.blocking import OpenJtalk, Synthesizer, UserDict, VoiceModel -__all__ = ["OpenJtalk", "UserDict"] +__all__ = ["OpenJtalk", "Synthesizer", "UserDict", "VoiceModel"] diff --git a/crates/voicevox_core_python_api/src/convert.rs b/crates/voicevox_core_python_api/src/convert.rs index 36d12f20a..4b908c48b 100644 --- a/crates/voicevox_core_python_api/src/convert.rs +++ b/crates/voicevox_core_python_api/src/convert.rs @@ -79,7 +79,33 @@ pub fn to_pydantic_dataclass(x: impl Serialize, class: &PyAny) -> PyResult<&PyAn class.call((), Some(x)) } -pub fn modify_accent_phrases<'py, Fun, Fut>( +pub(crate) fn blocking_modify_accent_phrases<'py>( + accent_phrases: &'py PyList, + speaker_id: StyleId, + py: Python<'py>, + method: impl FnOnce( + Vec, + StyleId, + ) -> voicevox_core::Result>, +) -> PyResult> { + let rust_accent_phrases = accent_phrases + .iter() + .map(from_dataclass) + .collect::>>()?; + + method(rust_accent_phrases, speaker_id) + .into_py_result(py)? + .iter() + .map(move |accent_phrase| { + to_pydantic_dataclass( + accent_phrase, + py.import("voicevox_core")?.getattr("AccentPhrase")?, + ) + }) + .collect() +} + +pub fn async_modify_accent_phrases<'py, Fun, Fut>( accent_phrases: &'py PyList, speaker_id: StyleId, py: Python<'py>, @@ -115,6 +141,7 @@ where }, ) } + pub fn to_rust_uuid(ob: &PyAny) -> PyResult { let uuid = ob.getattr("hex")?.extract::()?; uuid.parse::().into_py_value_result() diff --git a/crates/voicevox_core_python_api/src/lib.rs b/crates/voicevox_core_python_api/src/lib.rs index 54a78a2d8..f306e475c 100644 --- a/crates/voicevox_core_python_api/src/lib.rs +++ b/crates/voicevox_core_python_api/src/lib.rs @@ -356,7 +356,7 @@ impl Synthesizer { py: Python<'py>, ) -> PyResult<&'py PyAny> { let synthesizer = self.synthesizer.get()?.clone(); - modify_accent_phrases( + async_modify_accent_phrases( accent_phrases, StyleId::new(style_id), py, @@ -371,7 +371,7 @@ impl Synthesizer { py: Python<'py>, ) -> PyResult<&'py PyAny> { let synthesizer = self.synthesizer.get()?.clone(); - modify_accent_phrases( + async_modify_accent_phrases( accent_phrases, StyleId::new(style_id), py, @@ -386,7 +386,7 @@ impl Synthesizer { py: Python<'py>, ) -> PyResult<&'py PyAny> { let synthesizer = self.synthesizer.get()?.clone(); - modify_accent_phrases( + async_modify_accent_phrases( accent_phrases, StyleId::new(style_id), py, @@ -633,13 +633,42 @@ mod blocking { use pyo3::{ pyclass, pymethods, - types::{IntoPyDict as _, PyDict}, - PyObject, PyResult, Python, + types::{IntoPyDict as _, PyBytes, PyDict, PyList}, + PyAny, PyObject, PyRef, PyResult, Python, }; use uuid::Uuid; - use voicevox_core::UserDictWord; + use voicevox_core::{ + AccelerationMode, AudioQueryModel, InitializeOptions, StyleId, SynthesisOptions, + TtsOptions, UserDictWord, VoiceModelId, + }; + + use crate::{convert::VoicevoxCoreResultExt as _, Closable}; + + #[pyclass] + #[derive(Clone)] + struct VoiceModel(voicevox_core::blocking::VoiceModel); + + #[pymethods] + impl VoiceModel { + #[staticmethod] + fn from_path( + py: Python<'_>, + #[pyo3(from_py_with = "crate::convert::from_utf8_path")] path: String, + ) -> PyResult { + let inner = voicevox_core::blocking::VoiceModel::from_path(path).into_py_result(py)?; + Ok(Self(inner)) + } - use crate::convert::VoicevoxCoreResultExt as _; + #[getter] + fn id(&self) -> &str { + self.0.id().raw_voice_model_id() + } + + #[getter] + fn metas<'py>(&self, py: Python<'py>) -> Vec<&'py PyAny> { + crate::convert::to_pydantic_voice_model_meta(self.0.metas(), py).unwrap() + } + } #[pyclass] #[derive(Clone)] @@ -666,6 +695,280 @@ mod blocking { } } + #[pyclass] + struct Synthesizer( + Closable, Self>, + ); + + #[pymethods] + impl Synthesizer { + #[new] + #[pyo3(signature =( + open_jtalk, + acceleration_mode = InitializeOptions::default().acceleration_mode, + cpu_num_threads = InitializeOptions::default().cpu_num_threads, + ))] + fn new( + open_jtalk: OpenJtalk, + #[pyo3(from_py_with = "crate::convert::from_acceleration_mode")] + acceleration_mode: AccelerationMode, + cpu_num_threads: u16, + py: Python<'_>, + ) -> PyResult { + let inner = voicevox_core::blocking::Synthesizer::new( + open_jtalk.open_jtalk.clone(), + &InitializeOptions { + acceleration_mode, + cpu_num_threads, + }, + ) + .into_py_result(py)?; + Ok(Self(Closable::new(inner))) + } + + fn __repr__(&self) -> &'static str { + "Synthesizer { .. }" + } + + fn __enter__(slf: PyRef<'_, Self>) -> PyResult> { + slf.0.get()?; + Ok(slf) + } + + fn __exit__( + &mut self, + #[allow(unused_variables)] exc_type: &PyAny, + #[allow(unused_variables)] exc_value: &PyAny, + #[allow(unused_variables)] traceback: &PyAny, + ) { + self.close(); + } + + #[getter] + fn is_gpu_mode(&self) -> PyResult { + let inner = self.0.get()?; + Ok(inner.is_gpu_mode()) + } + + #[getter] + fn metas<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = self.0.get()?; + crate::convert::to_pydantic_voice_model_meta(&inner.metas(), py) + } + + fn load_voice_model(&mut self, model: &PyAny, py: Python<'_>) -> PyResult<()> { + let model: VoiceModel = model.extract()?; + self.0.get()?.load_voice_model(&model.0).into_py_result(py) + } + + fn unload_voice_model(&mut self, voice_model_id: &str, py: Python<'_>) -> PyResult<()> { + self.0 + .get()? + .unload_voice_model(&VoiceModelId::new(voice_model_id.to_string())) + .into_py_result(py) + } + + fn is_loaded_voice_model(&self, voice_model_id: &str) -> PyResult { + Ok(self + .0 + .get()? + .is_loaded_voice_model(&VoiceModelId::new(voice_model_id.to_string()))) + } + + fn audio_query_from_kana<'py>( + &self, + kana: &str, + style_id: u32, + py: Python<'py>, + ) -> PyResult<&'py PyAny> { + let inner = self.0.get()?; + + let audio_query = inner + .audio_query_from_kana(kana, StyleId::new(style_id)) + .into_py_result(py)?; + + let class = py.import("voicevox_core")?.getattr("AudioQuery")?; + crate::convert::to_pydantic_dataclass(audio_query, class) + } + + fn audio_query<'py>( + &self, + text: &str, + style_id: u32, + py: Python<'py>, + ) -> PyResult<&'py PyAny> { + let inner = self.0.get()?; + + let audio_query = inner + .audio_query(text, StyleId::new(style_id)) + .into_py_result(py)?; + + let class = py.import("voicevox_core")?.getattr("AudioQuery")?; + crate::convert::to_pydantic_dataclass(audio_query, class) + } + + fn create_accent_phrases_from_kana<'py>( + &self, + kana: &str, + style_id: u32, + py: Python<'py>, + ) -> PyResult> { + let inner = self.0.get()?; + + let accent_phrases = inner + .create_accent_phrases_from_kana(kana, StyleId::new(style_id)) + .into_py_result(py)?; + + let class = py.import("voicevox_core")?.getattr("AccentPhrase")?; + accent_phrases + .iter() + .map(|ap| crate::convert::to_pydantic_dataclass(ap, class)) + .collect() + } + + fn create_accent_phrases<'py>( + &self, + text: &str, + style_id: u32, + py: Python<'py>, + ) -> PyResult> { + let inner = self.0.get()?; + + let accent_phrases = inner + .create_accent_phrases(text, StyleId::new(style_id)) + .into_py_result(py)?; + + let class = py.import("voicevox_core")?.getattr("AccentPhrase")?; + accent_phrases + .iter() + .map(|ap| crate::convert::to_pydantic_dataclass(ap, class)) + .collect() + } + + fn replace_mora_data<'py>( + &self, + accent_phrases: &'py PyList, + style_id: u32, + py: Python<'py>, + ) -> PyResult> { + let inner = self.0.get()?; + crate::convert::blocking_modify_accent_phrases( + accent_phrases, + StyleId::new(style_id), + py, + |a, s| inner.replace_mora_data(&a, s), + ) + } + + fn replace_phoneme_length<'py>( + &self, + accent_phrases: &'py PyList, + style_id: u32, + py: Python<'py>, + ) -> PyResult> { + let inner = self.0.get()?; + crate::convert::blocking_modify_accent_phrases( + accent_phrases, + StyleId::new(style_id), + py, + |a, s| inner.replace_phoneme_length(&a, s), + ) + } + + fn replace_mora_pitch<'py>( + &self, + accent_phrases: &'py PyList, + style_id: u32, + py: Python<'py>, + ) -> PyResult> { + let inner = self.0.get()?; + crate::convert::blocking_modify_accent_phrases( + accent_phrases, + StyleId::new(style_id), + py, + |a, s| inner.replace_mora_pitch(&a, s), + ) + } + + #[pyo3(signature=( + audio_query, + style_id, + enable_interrogative_upspeak = TtsOptions::default().enable_interrogative_upspeak + ))] + fn synthesis<'py>( + &self, + #[pyo3(from_py_with = "crate::convert::from_dataclass")] audio_query: AudioQueryModel, + style_id: u32, + enable_interrogative_upspeak: bool, + py: Python<'py>, + ) -> PyResult<&'py PyBytes> { + let wav = &self + .0 + .get()? + .synthesis( + &audio_query, + StyleId::new(style_id), + &SynthesisOptions { + enable_interrogative_upspeak, + }, + ) + .into_py_result(py)?; + Ok(PyBytes::new(py, wav)) + } + + #[pyo3(signature=( + kana, + style_id, + enable_interrogative_upspeak = TtsOptions::default().enable_interrogative_upspeak + ))] + fn tts_from_kana<'py>( + &self, + kana: &str, + style_id: u32, + enable_interrogative_upspeak: bool, + py: Python<'py>, + ) -> PyResult<&'py PyBytes> { + let style_id = StyleId::new(style_id); + let options = &TtsOptions { + enable_interrogative_upspeak, + }; + let wav = &self + .0 + .get()? + .tts_from_kana(kana, style_id, options) + .into_py_result(py)?; + Ok(PyBytes::new(py, wav)) + } + + #[pyo3(signature=( + text, + style_id, + enable_interrogative_upspeak = TtsOptions::default().enable_interrogative_upspeak + ))] + fn tts<'py>( + &self, + text: &str, + style_id: u32, + enable_interrogative_upspeak: bool, + py: Python<'py>, + ) -> PyResult<&'py PyBytes> { + let style_id = StyleId::new(style_id); + let options = &TtsOptions { + enable_interrogative_upspeak, + }; + let wav = &self + .0 + .get()? + .tts(text, style_id, options) + .into_py_result(py)?; + Ok(PyBytes::new(py, wav)) + } + + fn close(&mut self) { + self.0.close() + } + } + #[pyclass] #[derive(Default, Debug, Clone)] pub(crate) struct UserDict { From 9da8b12ff24dae0d6344a65f2a6cc8e231b48117 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Thu, 7 Dec 2023 01:02:53 +0900 Subject: [PATCH 06/13] `add_class` --- crates/voicevox_core_python_api/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/voicevox_core_python_api/src/lib.rs b/crates/voicevox_core_python_api/src/lib.rs index f306e475c..736e72ee6 100644 --- a/crates/voicevox_core_python_api/src/lib.rs +++ b/crates/voicevox_core_python_api/src/lib.rs @@ -30,7 +30,9 @@ fn rust(py: Python<'_>, module: &PyModule) -> PyResult<()> { add_exceptions(module)?; let blocking_module = PyModule::new(py, "voicevox_core._rust.blocking")?; + blocking_module.add_class::()?; blocking_module.add_class::()?; + blocking_module.add_class::()?; blocking_module.add_class::()?; module.add_and_register_submodule(blocking_module)?; @@ -646,7 +648,7 @@ mod blocking { #[pyclass] #[derive(Clone)] - struct VoiceModel(voicevox_core::blocking::VoiceModel); + pub(crate) struct VoiceModel(voicevox_core::blocking::VoiceModel); #[pymethods] impl VoiceModel { @@ -696,7 +698,7 @@ mod blocking { } #[pyclass] - struct Synthesizer( + pub(crate) struct Synthesizer( Closable, Self>, ); From 16b8428124717b8839847b46a1ccffe59ed0a6cc Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Fri, 8 Dec 2023 00:20:24 +0900 Subject: [PATCH 07/13] =?UTF-8?q?example/python/run.py=E3=82=92=E3=83=96?= =?UTF-8?q?=E3=83=AD=E3=83=83=E3=82=AD=E3=83=B3=E3=82=B0=E7=89=88=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example/python/README.md | 2 +- example/python/run-asyncio.py | 100 ++++++++++++++++++++++++++++++++++ example/python/run.py | 18 +++--- 3 files changed, 109 insertions(+), 11 deletions(-) create mode 100644 example/python/run-asyncio.py diff --git a/example/python/README.md b/example/python/README.md index e2097e137..392896030 100644 --- a/example/python/README.md +++ b/example/python/README.md @@ -47,7 +47,7 @@ TODO: ## 実行 -run.py を実行します。 Open JTalk 辞書ディレクトリ、読み上げさせたい文章、出力 wav ファイルのパスをオプションで指定することができます。 +run.py もしくは run-asyncio.py を実行します。 Open JTalk 辞書ディレクトリ、読み上げさせたい文章、出力 wav ファイルのパスをオプションで指定することができます。 ```console ❯ python ./run.py -h diff --git a/example/python/run-asyncio.py b/example/python/run-asyncio.py new file mode 100644 index 000000000..28926b127 --- /dev/null +++ b/example/python/run-asyncio.py @@ -0,0 +1,100 @@ +import asyncio +import dataclasses +import json +import logging +from argparse import ArgumentParser +from pathlib import Path +from typing import Tuple + +import voicevox_core +from voicevox_core import AccelerationMode, AudioQuery +from voicevox_core.asyncio import OpenJtalk, Synthesizer, VoiceModel + + +# asyncやawaitは必須です。 +async def main() -> None: + logging.basicConfig(format="[%(levelname)s] %(name)s: %(message)s") + logger = logging.getLogger(__name__) + logger.setLevel("DEBUG") + logging.getLogger("voicevox_core_python_api").setLevel("DEBUG") + logging.getLogger("voicevox_core").setLevel("DEBUG") + + ( + acceleration_mode, + vvm_path, + open_jtalk_dict_dir, + text, + out, + style_id, + ) = parse_args() + + logger.debug("%s", f"{voicevox_core.supported_devices()=}") + + logger.info("%s", f"Initializing ({acceleration_mode=}, {open_jtalk_dict_dir=})") + synthesizer = Synthesizer( + await OpenJtalk.new(open_jtalk_dict_dir), acceleration_mode=acceleration_mode + ) + + logger.debug("%s", f"{synthesizer.metas=}") + 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) + + logger.info("%s", f"Creating an AudioQuery from {text!r}") + audio_query = await synthesizer.audio_query(text, style_id) + + logger.info("%s", f"Synthesizing with {display_as_json(audio_query)}") + wav = await synthesizer.synthesis(audio_query, style_id) + + out.write_bytes(wav) + logger.info("%s", f"Wrote `{out}`") + + +def parse_args() -> Tuple[AccelerationMode, Path, Path, str, Path, int]: + argparser = ArgumentParser() + argparser.add_argument( + "--mode", + default="AUTO", + type=AccelerationMode, + help='モード ("AUTO", "CPU", "GPU")', + ) + argparser.add_argument( + "vvm", + type=Path, + help="vvmファイルへのパス", + ) + argparser.add_argument( + "--dict-dir", + default="./open_jtalk_dic_utf_8-1.11", + type=Path, + help="Open JTalkの辞書ディレクトリ", + ) + argparser.add_argument( + "--text", + default="この音声は、ボイスボックスを使用して、出力されています。", + help="読み上げさせたい文章", + ) + argparser.add_argument( + "--out", + default="./output.wav", + type=Path, + help="出力wavファイルのパス", + ) + argparser.add_argument( + "--style-id", + default=0, + type=int, + help="話者IDを指定", + ) + args = argparser.parse_args() + return (args.mode, args.vvm, args.dict_dir, args.text, args.out, args.style_id) + + +def display_as_json(audio_query: AudioQuery) -> str: + return json.dumps(dataclasses.asdict(audio_query), ensure_ascii=False) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/example/python/run.py b/example/python/run.py index 28926b127..a57139b1c 100644 --- a/example/python/run.py +++ b/example/python/run.py @@ -1,4 +1,3 @@ -import asyncio import dataclasses import json import logging @@ -8,11 +7,10 @@ import voicevox_core from voicevox_core import AccelerationMode, AudioQuery -from voicevox_core.asyncio import OpenJtalk, Synthesizer, VoiceModel +from voicevox_core.blocking import OpenJtalk, Synthesizer, VoiceModel -# asyncやawaitは必須です。 -async def main() -> None: +def main() -> None: logging.basicConfig(format="[%(levelname)s] %(name)s: %(message)s") logger = logging.getLogger(__name__) logger.setLevel("DEBUG") @@ -32,21 +30,21 @@ async def main() -> None: logger.info("%s", f"Initializing ({acceleration_mode=}, {open_jtalk_dict_dir=})") synthesizer = Synthesizer( - await OpenJtalk.new(open_jtalk_dict_dir), acceleration_mode=acceleration_mode + OpenJtalk(open_jtalk_dict_dir), acceleration_mode=acceleration_mode ) logger.debug("%s", f"{synthesizer.metas=}") 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) + model = VoiceModel.from_path(vvm_path) + synthesizer.load_voice_model(model) logger.info("%s", f"Creating an AudioQuery from {text!r}") - audio_query = await synthesizer.audio_query(text, style_id) + audio_query = synthesizer.audio_query(text, style_id) logger.info("%s", f"Synthesizing with {display_as_json(audio_query)}") - wav = await synthesizer.synthesis(audio_query, style_id) + wav = synthesizer.synthesis(audio_query, style_id) out.write_bytes(wav) logger.info("%s", f"Wrote `{out}`") @@ -97,4 +95,4 @@ def display_as_json(audio_query: AudioQuery) -> str: if __name__ == "__main__": - asyncio.run(main()) + main() From 9c99edced272827b4de441c58667acf59ff55b6d Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Sat, 9 Dec 2023 00:37:47 +0900 Subject: [PATCH 08/13] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=82=92?= =?UTF-8?q?=E3=82=B3=E3=83=94=E3=83=9A=E3=81=A7=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...load.py => test_asyncio_user_dict_load.py} | 7 +- ...y => test_asyncio_user_dict_manipulate.py} | 7 +- .../test/test_blocking_user_dict_load.py | 40 +++++++++ .../test_blocking_user_dict_manipulate.py | 83 +++++++++++++++++++ ...st_pseudo_raii_for_asyncio_synthesizer.py} | 2 + ...st_pseudo_raii_for_blocking_synthesizer.py | 46 ++++++++++ 6 files changed, 183 insertions(+), 2 deletions(-) rename crates/voicevox_core_python_api/python/test/{test_user_dict_load.py => test_asyncio_user_dict_load.py} (89%) rename crates/voicevox_core_python_api/python/test/{test_user_dict_manipulate.py => test_asyncio_user_dict_manipulate.py} (94%) create mode 100644 crates/voicevox_core_python_api/python/test/test_blocking_user_dict_load.py create mode 100644 crates/voicevox_core_python_api/python/test/test_blocking_user_dict_manipulate.py rename crates/voicevox_core_python_api/python/test/{test_pseudo_raii_for_synthesizer.py => test_pseudo_raii_for_asyncio_synthesizer.py} (94%) create mode 100644 crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_blocking_synthesizer.py diff --git a/crates/voicevox_core_python_api/python/test/test_user_dict_load.py b/crates/voicevox_core_python_api/python/test/test_asyncio_user_dict_load.py similarity index 89% rename from crates/voicevox_core_python_api/python/test/test_user_dict_load.py rename to crates/voicevox_core_python_api/python/test/test_asyncio_user_dict_load.py index e91773666..c509b8c2d 100644 --- a/crates/voicevox_core_python_api/python/test/test_user_dict_load.py +++ b/crates/voicevox_core_python_api/python/test/test_asyncio_user_dict_load.py @@ -1,4 +1,9 @@ -# ユーザー辞書の単語が反映されるかをテストする。 +""" +ユーザー辞書の単語が反映されるかをテストする。 + +``test_pseudo_raii_for_blocking_synthesizer`` と対になる。 +""" + # AudioQueryのkanaを比較して変化するかどうかで判断する。 from uuid import UUID diff --git a/crates/voicevox_core_python_api/python/test/test_user_dict_manipulate.py b/crates/voicevox_core_python_api/python/test/test_asyncio_user_dict_manipulate.py similarity index 94% rename from crates/voicevox_core_python_api/python/test/test_user_dict_manipulate.py rename to crates/voicevox_core_python_api/python/test/test_asyncio_user_dict_manipulate.py index 652ddfc7a..493fade53 100644 --- a/crates/voicevox_core_python_api/python/test/test_user_dict_manipulate.py +++ b/crates/voicevox_core_python_api/python/test/test_asyncio_user_dict_manipulate.py @@ -1,4 +1,9 @@ -# ユーザー辞書の操作をテストする。 +""" +ユーザー辞書の操作をテストする。 + +``test_blocking_user_dict_manipulate`` と対になる。 +""" + # どのコードがどの操作を行っているかはコメントを参照。 import os 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 new file mode 100644 index 000000000..ef94d9742 --- /dev/null +++ b/crates/voicevox_core_python_api/python/test/test_blocking_user_dict_load.py @@ -0,0 +1,40 @@ +""" +ユーザー辞書の単語が反映されるかをテストする。 + +``test_pseudo_raii_for_asyncio_synthesizer`` と対になる。 +""" + +# AudioQueryのkanaを比較して変化するかどうかで判断する。 + +from uuid import UUID + +import conftest +import voicevox_core + + +def test_user_dict_load() -> None: + open_jtalk = voicevox_core.blocking.OpenJtalk(conftest.open_jtalk_dic_dir) + model = voicevox_core.blocking.VoiceModel.from_path(conftest.model_dir) + synthesizer = voicevox_core.blocking.Synthesizer(open_jtalk) + + synthesizer.load_voice_model(model) + + audio_query_without_dict = synthesizer.audio_query( + "this_word_should_not_exist_in_default_dictionary", style_id=0 + ) + + temp_dict = voicevox_core.blocking.UserDict() + uuid = temp_dict.add_word( + voicevox_core.UserDictWord( + surface="this_word_should_not_exist_in_default_dictionary", + pronunciation="アイウエオ", + ) + ) + assert isinstance(uuid, UUID) + + open_jtalk.use_user_dict(temp_dict) + + audio_query_with_dict = synthesizer.audio_query( + "this_word_should_not_exist_in_default_dictionary", style_id=0 + ) + assert audio_query_without_dict != audio_query_with_dict diff --git a/crates/voicevox_core_python_api/python/test/test_blocking_user_dict_manipulate.py b/crates/voicevox_core_python_api/python/test/test_blocking_user_dict_manipulate.py new file mode 100644 index 000000000..23131ec49 --- /dev/null +++ b/crates/voicevox_core_python_api/python/test/test_blocking_user_dict_manipulate.py @@ -0,0 +1,83 @@ +""" +ユーザー辞書の操作をテストする。 + +``test_asyncio_user_dict_manipulate`` と対になる。 +""" + +# どのコードがどの操作を行っているかはコメントを参照。 + +import os +import tempfile +from uuid import UUID + +import pydantic +import pytest +import voicevox_core + + +def test_user_dict_load() -> None: + dict_a = voicevox_core.blocking.UserDict() + + # 単語の追加 + uuid_a = dict_a.add_word( + voicevox_core.UserDictWord( + surface="hoge", + pronunciation="ホゲ", + ) + ) + assert isinstance(uuid_a, UUID) + assert dict_a.words[uuid_a].surface == "hoge" + assert dict_a.words[uuid_a].pronunciation == "ホゲ" + + # 単語の更新 + dict_a.update_word( + uuid_a, + voicevox_core.UserDictWord( + surface="fuga", + pronunciation="フガ", + ), + ) + + assert dict_a.words[uuid_a].surface == "fuga" + assert dict_a.words[uuid_a].pronunciation == "フガ" + + # ユーザー辞書のインポート + dict_b = voicevox_core.blocking.UserDict() + uuid_b = dict_b.add_word( + voicevox_core.UserDictWord( + surface="foo", + pronunciation="フー", + ) + ) + + dict_a.import_dict(dict_b) + assert uuid_b in dict_a.words + + # ユーザー辞書のエクスポート + dict_c = voicevox_core.blocking.UserDict() + uuid_c = dict_c.add_word( + voicevox_core.UserDictWord( + surface="bar", + pronunciation="バー", + ) + ) + temp_path_fd, temp_path = tempfile.mkstemp() + os.close(temp_path_fd) + dict_c.save(temp_path) + dict_a.load(temp_path) + assert uuid_a in dict_a.words + assert uuid_c in dict_a.words + + # 単語の削除 + dict_a.remove_word(uuid_a) + assert uuid_a not in dict_a.words + assert uuid_c in dict_a.words + + # 単語のバリデーション + with pytest.raises(pydantic.ValidationError): + dict_a.add_word( + voicevox_core.UserDictWord( + surface="", + pronunciation="カタカナ以外の文字", + ) + ) 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_asyncio_synthesizer.py similarity index 94% rename from crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py rename to crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_asyncio_synthesizer.py index 69ff89d2a..93d92ad28 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_asyncio_synthesizer.py @@ -1,5 +1,7 @@ """ ``Synthesizer`` について、(広義の)RAIIができることをテストする。 + +``test_pseudo_raii_for_blocking_synthesizer`` と対になる。 """ import conftest diff --git a/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_blocking_synthesizer.py b/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_blocking_synthesizer.py new file mode 100644 index 000000000..3e3f5f823 --- /dev/null +++ b/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_blocking_synthesizer.py @@ -0,0 +1,46 @@ +""" +``Synthesizer`` について、(広義の)RAIIができることをテストする。 + +``test_pseudo_raii_for_asyncio_synthesizer`` と対になる。 +""" + +import conftest +import pytest +from voicevox_core.blocking import OpenJtalk, Synthesizer + + +def test_enter_returns_workable_self(synthesizer: Synthesizer) -> None: + with synthesizer as ctx: + assert ctx is synthesizer + _ = synthesizer.metas + + +def test_closing_multiple_times_is_allowed(synthesizer: Synthesizer) -> None: + with synthesizer: + with synthesizer: + pass + synthesizer.close() + synthesizer.close() + + +def test_access_after_close_denied(synthesizer: Synthesizer) -> None: + synthesizer.close() + with pytest.raises(ValueError, match="^The `Synthesizer` is closed$"): + _ = synthesizer.metas + + +def test_access_after_exit_denied(synthesizer: Synthesizer) -> None: + with synthesizer: + pass + with pytest.raises(ValueError, match="^The `Synthesizer` is closed$"): + _ = synthesizer.metas + + +@pytest.fixture +def synthesizer(open_jtalk: OpenJtalk) -> Synthesizer: + return Synthesizer(open_jtalk) + + +@pytest.fixture(scope="session") +def open_jtalk() -> OpenJtalk: + return OpenJtalk(conftest.open_jtalk_dic_dir) From d7fed1f9c9a0bcda653163fbf3127c9ba7c8ae39 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Sat, 9 Dec 2023 00:46:11 +0900 Subject: [PATCH 09/13] =?UTF-8?q?#701=20=E3=81=AE=E5=8B=95=E4=BD=9C?= =?UTF-8?q?=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF=E3=81=ABrun-asyncio.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 --- .github/workflows/test.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8987d8f5f..159fd6a23 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -292,7 +292,10 @@ jobs: poetry run pytest - name: Exampleを実行 - run: poetry run python ../../example/python/run.py ../../model/sample.vvm --dict-dir ../test_util/data/open_jtalk_dic_utf_8-1.11 + run: | + for file in ../../example/python/run{,-asyncio}.py; do + poetry run python "$file" ../../model/sample.vvm --dict-dir ../test_util/data/open_jtalk_dic_utf_8-1.11 + done build-and-test-java-api: strategy: fail-fast: false From a2a8143dfb37c0e7d9de55d9606d5fefd6d3b149 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Sat, 9 Dec 2023 01:00:09 +0900 Subject: [PATCH 10/13] `shell: bash` --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 159fd6a23..3e47256f3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -262,6 +262,7 @@ jobs: runs-on: ${{ matrix.os }} defaults: run: + shell: bash working-directory: ./crates/voicevox_core_python_api steps: - uses: actions/checkout@v3 From 5000662d3e4064e737b185cd4b556ea01726e57b Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Sat, 9 Dec 2023 01:01:44 +0900 Subject: [PATCH 11/13] =?UTF-8?q?#699=20=E3=81=AEusage.md=E3=82=92?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/usage.md | 43 +++++++++++++++++++------------------------ 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index e602e72fd..3e19b31ae 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -58,28 +58,23 @@ VOICEVOX コアでは`Synthesizer`に音声モデルを読み込むことでテ これは Python で書かれたサンプルコードですが、大枠の流れはどの言語でも同じです。 ```python -import asyncio from pprint import pprint -from voicevox_core import OpenJtalk, Synthesizer, VoiceModel - -# asyncやawaitは必須です -async def main(): - # 1. Synthesizerの初期化 - open_jtalk_dict_dir = "open_jtalk_dic_utf_8-1.11" - synthesizer = Synthesizer(await OpenJtalk.new(open_jtalk_dict_dir)) - - # 2. 音声モデルの読み込み - model = await VoiceModel.from_path("model/0.vvm") - await synthesizer.load_voice_model(model) - - # 3. テキスト音声合成 - text = "サンプル音声です" - style_id = 0 - wav = await synthesizer.tts(text, style_id) - with open("output.wav", "wb") as f: - f.write(wav) - -asyncio.run(main()) +from voicevox_core.blocking import OpenJtalk, Synthesizer, VoiceModel + +# 1. Synthesizerの初期化 +open_jtalk_dict_dir = "open_jtalk_dic_utf_8-1.11" +synthesizer = Synthesizer(OpenJtalk(open_jtalk_dict_dir)) + +# 2. 音声モデルの読み込み +model = VoiceModel.from_path("model/0.vvm") +synthesizer.load_voice_model(model) + +# 3. テキスト音声合成 +text = "サンプル音声です" +style_id = 0 +wav = synthesizer.tts(text, style_id) +with open("output.wav", "wb") as f: + f.write(wav) ``` ### 1. Synthesizer の初期化 @@ -91,7 +86,7 @@ asyncio.run(main()) VVM ファイルから`VoiceModel`インスタンスを作成し、`Synthesizer`に読み込ませます。その VVM ファイルにどの声が含まれているかは`VoiceModel`の`.metas`で確認できます。 ```python -model = await VoiceModel.from_path("model/0.vvm") +model = VoiceModel.from_path("model/0.vvm") pprint(model.metas) ``` @@ -122,7 +117,7 @@ pprint(model.metas) ```python text = "サンプル音声です" style_id = 0 -audio_query = await synthesizer.audio_query(text, style_id) +audio_query = synthesizer.audio_query(text, style_id) pprint(audio_query) ``` @@ -156,7 +151,7 @@ audio_query.pitch_scale += 0.1 調整した`AudioQuery`を`Synthesizer`の`.synthesis`に渡すと、調整した音声波形のバイナリデータが返ります。 ```python -wav = await synthesizer.synthesis(audio_query, style_id) +wav = synthesizer.synthesis(audio_query, style_id) with open("output.wav", "wb") as f: f.write(wav) ``` From d5083e575f33b63b69f793e4ece6a78fbf59cc5f Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Sun, 10 Dec 2023 01:42:33 +0900 Subject: [PATCH 12/13] =?UTF-8?q?=E3=82=B9=E3=82=BF=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=81=AE=E7=B5=B1=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/voicevox_core_python_api/src/lib.rs | 80 ++++++++++++---------- 1 file changed, 45 insertions(+), 35 deletions(-) diff --git a/crates/voicevox_core_python_api/src/lib.rs b/crates/voicevox_core_python_api/src/lib.rs index 736e72ee6..655487148 100644 --- a/crates/voicevox_core_python_api/src/lib.rs +++ b/crates/voicevox_core_python_api/src/lib.rs @@ -648,7 +648,9 @@ mod blocking { #[pyclass] #[derive(Clone)] - pub(crate) struct VoiceModel(voicevox_core::blocking::VoiceModel); + pub(crate) struct VoiceModel { + model: voicevox_core::blocking::VoiceModel, + } #[pymethods] impl VoiceModel { @@ -657,18 +659,18 @@ mod blocking { py: Python<'_>, #[pyo3(from_py_with = "crate::convert::from_utf8_path")] path: String, ) -> PyResult { - let inner = voicevox_core::blocking::VoiceModel::from_path(path).into_py_result(py)?; - Ok(Self(inner)) + let model = voicevox_core::blocking::VoiceModel::from_path(path).into_py_result(py)?; + Ok(Self { model }) } #[getter] fn id(&self) -> &str { - self.0.id().raw_voice_model_id() + self.model.id().raw_voice_model_id() } #[getter] fn metas<'py>(&self, py: Python<'py>) -> Vec<&'py PyAny> { - crate::convert::to_pydantic_voice_model_meta(self.0.metas(), py).unwrap() + crate::convert::to_pydantic_voice_model_meta(self.model.metas(), py).unwrap() } } @@ -698,9 +700,12 @@ mod blocking { } #[pyclass] - pub(crate) struct Synthesizer( - Closable, Self>, - ); + pub(crate) struct Synthesizer { + synthesizer: Closable< + voicevox_core::blocking::Synthesizer, + Self, + >, + } #[pymethods] impl Synthesizer { @@ -725,7 +730,9 @@ mod blocking { }, ) .into_py_result(py)?; - Ok(Self(Closable::new(inner))) + Ok(Self { + synthesizer: Closable::new(inner), + }) } fn __repr__(&self) -> &'static str { @@ -733,7 +740,7 @@ mod blocking { } fn __enter__(slf: PyRef<'_, Self>) -> PyResult> { - slf.0.get()?; + slf.synthesizer.get()?; Ok(slf) } @@ -748,23 +755,26 @@ mod blocking { #[getter] fn is_gpu_mode(&self) -> PyResult { - let inner = self.0.get()?; - Ok(inner.is_gpu_mode()) + let synthesizer = self.synthesizer.get()?; + Ok(synthesizer.is_gpu_mode()) } #[getter] fn metas<'py>(&self, py: Python<'py>) -> PyResult> { - let inner = self.0.get()?; - crate::convert::to_pydantic_voice_model_meta(&inner.metas(), py) + let synthesizer = self.synthesizer.get()?; + 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.0.get()?.load_voice_model(&model.0).into_py_result(py) + self.synthesizer + .get()? + .load_voice_model(&model.model) + .into_py_result(py) } fn unload_voice_model(&mut self, voice_model_id: &str, py: Python<'_>) -> PyResult<()> { - self.0 + self.synthesizer .get()? .unload_voice_model(&VoiceModelId::new(voice_model_id.to_string())) .into_py_result(py) @@ -772,7 +782,7 @@ mod blocking { fn is_loaded_voice_model(&self, voice_model_id: &str) -> PyResult { Ok(self - .0 + .synthesizer .get()? .is_loaded_voice_model(&VoiceModelId::new(voice_model_id.to_string()))) } @@ -783,9 +793,9 @@ mod blocking { style_id: u32, py: Python<'py>, ) -> PyResult<&'py PyAny> { - let inner = self.0.get()?; + let synthesizer = self.synthesizer.get()?; - let audio_query = inner + let audio_query = synthesizer .audio_query_from_kana(kana, StyleId::new(style_id)) .into_py_result(py)?; @@ -799,9 +809,9 @@ mod blocking { style_id: u32, py: Python<'py>, ) -> PyResult<&'py PyAny> { - let inner = self.0.get()?; + let synthesizesr = self.synthesizer.get()?; - let audio_query = inner + let audio_query = synthesizesr .audio_query(text, StyleId::new(style_id)) .into_py_result(py)?; @@ -815,9 +825,9 @@ mod blocking { style_id: u32, py: Python<'py>, ) -> PyResult> { - let inner = self.0.get()?; + let synthesizer = self.synthesizer.get()?; - let accent_phrases = inner + let accent_phrases = synthesizer .create_accent_phrases_from_kana(kana, StyleId::new(style_id)) .into_py_result(py)?; @@ -834,9 +844,9 @@ mod blocking { style_id: u32, py: Python<'py>, ) -> PyResult> { - let inner = self.0.get()?; + let synthesizer = self.synthesizer.get()?; - let accent_phrases = inner + let accent_phrases = synthesizer .create_accent_phrases(text, StyleId::new(style_id)) .into_py_result(py)?; @@ -853,12 +863,12 @@ mod blocking { style_id: u32, py: Python<'py>, ) -> PyResult> { - let inner = self.0.get()?; + let synthesizer = self.synthesizer.get()?; crate::convert::blocking_modify_accent_phrases( accent_phrases, StyleId::new(style_id), py, - |a, s| inner.replace_mora_data(&a, s), + |a, s| synthesizer.replace_mora_data(&a, s), ) } @@ -868,12 +878,12 @@ mod blocking { style_id: u32, py: Python<'py>, ) -> PyResult> { - let inner = self.0.get()?; + let synthesizer = self.synthesizer.get()?; crate::convert::blocking_modify_accent_phrases( accent_phrases, StyleId::new(style_id), py, - |a, s| inner.replace_phoneme_length(&a, s), + |a, s| synthesizer.replace_phoneme_length(&a, s), ) } @@ -883,12 +893,12 @@ mod blocking { style_id: u32, py: Python<'py>, ) -> PyResult> { - let inner = self.0.get()?; + let synthesizer = self.synthesizer.get()?; crate::convert::blocking_modify_accent_phrases( accent_phrases, StyleId::new(style_id), py, - |a, s| inner.replace_mora_pitch(&a, s), + |a, s| synthesizer.replace_mora_pitch(&a, s), ) } @@ -905,7 +915,7 @@ mod blocking { py: Python<'py>, ) -> PyResult<&'py PyBytes> { let wav = &self - .0 + .synthesizer .get()? .synthesis( &audio_query, @@ -935,7 +945,7 @@ mod blocking { enable_interrogative_upspeak, }; let wav = &self - .0 + .synthesizer .get()? .tts_from_kana(kana, style_id, options) .into_py_result(py)?; @@ -959,7 +969,7 @@ mod blocking { enable_interrogative_upspeak, }; let wav = &self - .0 + .synthesizer .get()? .tts(text, style_id, options) .into_py_result(py)?; @@ -967,7 +977,7 @@ mod blocking { } fn close(&mut self) { - self.0.close() + self.synthesizer.close() } } From 6b74f6c543f76b188ca5d7b931e3e913203a4d6e Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Sun, 10 Dec 2023 02:13:02 +0900 Subject: [PATCH 13/13] =?UTF-8?q?run-asyncio.py=E3=81=AE=E3=82=B3=E3=83=A1?= =?UTF-8?q?=E3=83=B3=E3=83=88=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example/python/run-asyncio.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example/python/run-asyncio.py b/example/python/run-asyncio.py index 28926b127..70d204a92 100644 --- a/example/python/run-asyncio.py +++ b/example/python/run-asyncio.py @@ -1,3 +1,5 @@ +"""asyncio版のサンプルコードです。""" + import asyncio import dataclasses import json @@ -11,7 +13,6 @@ from voicevox_core.asyncio import OpenJtalk, Synthesizer, VoiceModel -# asyncやawaitは必須です。 async def main() -> None: logging.basicConfig(format="[%(levelname)s] %(name)s: %(message)s") logger = logging.getLogger(__name__)