From 00a1c53aaa39165922c57f608ff316a3fb933112 Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Sun, 10 Dec 2023 03:50:28 +0900 Subject: [PATCH] =?UTF-8?q?Python=E3=81=AE=E3=83=96=E3=83=AD=E3=83=83?= =?UTF-8?q?=E3=82=AD=E3=83=B3=E3=82=B0API=E3=82=92=E5=AE=9F=E8=A3=85=20(#7?= =?UTF-8?q?06)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * `blocking.{OpenJtalk,UserDict}` * `voicevox_core.asyncio` * Blackとisortに`--diff`を付ける * isort * `blocking.{Synthesizer,VoiceModel}` * `add_class` * example/python/run.pyをブロッキング版に * テストをコピペで実装 * #701 の動作チェックにrun-asyncio.pyを追加 * `shell: bash` * #699 のusage.mdを更新 * スタイルの統一 * run-asyncio.pyのコメント変更 --- .github/workflows/generate_document.yml | 3 + .github/workflows/python_lint.yml | 4 +- .github/workflows/test.yml | 6 +- ...load.py => test_asyncio_user_dict_load.py} | 15 +- ...y => test_asyncio_user_dict_manipulate.py} | 13 +- .../test/test_blocking_user_dict_load.py | 40 ++ .../test_blocking_user_dict_manipulate.py | 83 ++++ ...st_pseudo_raii_for_asyncio_synthesizer.py} | 4 +- ...st_pseudo_raii_for_blocking_synthesizer.py | 46 ++ .../python/voicevox_core/__init__.py | 12 +- .../python/voicevox_core/_rust/__init__.pyi | 111 +++++ .../{_rust.pyi => _rust/asyncio.pyi} | 109 ----- .../python/voicevox_core/_rust/blocking.pyi | 408 ++++++++++++++++ .../python/voicevox_core/asyncio.py | 3 + .../python/voicevox_core/blocking.py | 3 + .../voicevox_core_python_api/src/convert.rs | 31 +- crates/voicevox_core_python_api/src/lib.rs | 460 +++++++++++++++++- docs/apis/python_api/conf.py | 1 + docs/usage.md | 43 +- example/python/README.md | 2 +- example/python/run-asyncio.py | 101 ++++ example/python/run.py | 25 +- 22 files changed, 1341 insertions(+), 182 deletions(-) rename crates/voicevox_core_python_api/python/test/{test_user_dict_load.py => test_asyncio_user_dict_load.py} (68%) rename crates/voicevox_core_python_api/python/test/{test_user_dict_manipulate.py => test_asyncio_user_dict_manipulate.py} (88%) 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} (90%) create mode 100644 crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_blocking_synthesizer.py create mode 100644 crates/voicevox_core_python_api/python/voicevox_core/_rust/__init__.pyi rename crates/voicevox_core_python_api/python/voicevox_core/{_rust.pyi => _rust/asyncio.pyi} (80%) 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/asyncio.py create mode 100644 crates/voicevox_core_python_api/python/voicevox_core/blocking.py create mode 100644 example/python/run-asyncio.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/.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 . diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8987d8f5f..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 @@ -292,7 +293,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 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 68% 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 572046496..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 @@ -10,9 +15,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 +25,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_asyncio_user_dict_manipulate.py similarity index 88% 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 1ba37465f..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 @@ -12,7 +17,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 +43,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 +55,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/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 90% 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 a40c9c160..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,11 +1,13 @@ """ ``Synthesizer`` について、(広義の)RAIIができることをテストする。 + +``test_pseudo_raii_for_blocking_synthesizer`` と対になる。 """ 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_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) 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..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,27 +25,27 @@ ModelAlreadyLoadedError, ModelNotFoundError, NotLoadedOpenjtalkDictError, - OpenJtalk, OpenZipFileError, ParseKanaError, ReadZipEntryError, SaveUserDictError, StyleAlreadyLoadedError, StyleNotFoundError, - Synthesizer, - UserDict, UseUserDictError, - VoiceModel, WordNotFoundError, __version__, supported_devices, ) +from . import asyncio, blocking # noqa: F401 isort: skip + __all__ = [ "__version__", "AccelerationMode", "AccentPhrase", "AudioQuery", + "asyncio", + "blocking", "ExtractFullContextLabelError", "GetSupportedDevicesError", "GpuSupportError", @@ -57,7 +57,6 @@ "ModelNotFoundError", "Mora", "NotLoadedOpenjtalkDictError", - "OpenJtalk", "OpenZipFileError", "ParseKanaError", "ReadZipEntryError", @@ -68,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 new file mode 100644 index 000000000..3a47ef02b --- /dev/null +++ b/crates/voicevox_core_python_api/python/voicevox_core/_rust/__init__.pyi @@ -0,0 +1,111 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from voicevox_core import SupportedDevices + +__version__: str + +def supported_devices() -> SupportedDevices: + """ + このライブラリで利用可能なデバイスの情報を取得する。 + + .. code-block:: + + import voicevox_core + + supported_devices = voicevox_core.supported_devices() + """ + ... + +class NotLoadedOpenjtalkDictError(Exception): + """open_jtalk辞書ファイルが読み込まれていない。""" + + ... + +class GpuSupportError(Exception): + """GPUモードがサポートされていない。""" + + ... + +class OpenZipFileError(Exception): + """ZIPファイルを開くことに失敗した。""" + + ... + +class ReadZipEntryError(Exception): + """ZIP内のファイルが読めなかった。""" + + ... + +class ModelAlreadyLoadedError(Exception): + """すでに読み込まれている音声モデルを読み込もうとした。""" + + ... + +class StyleAlreadyLoadedError(Exception): + """すでに読み込まれているスタイルを読み込もうとした。""" + + ... + +class InvalidModelDataError(Exception): + """無効なモデルデータ。""" + + ... + +class GetSupportedDevicesError(Exception): + """サポートされているデバイス情報取得に失敗した。""" + + ... + +class StyleNotFoundError(KeyError): + """スタイルIDに対するスタイルが見つからなかった。""" + + ... + +class ModelNotFoundError(KeyError): + """音声モデルIDに対する音声モデルが見つからなかった。""" + + ... + +class InferenceFailedError(Exception): + """推論に失敗した。""" + + ... + +class ExtractFullContextLabelError(Exception): + """コンテキストラベル出力に失敗した。""" + + ... + +class ParseKanaError(ValueError): + """AquesTalk風記法のテキストの解析に失敗した。""" + + ... + +class LoadUserDictError(Exception): + """ユーザー辞書を読み込めなかった。""" + + ... + +class SaveUserDictError(Exception): + """ユーザー辞書を書き込めなかった。""" + + ... + +class WordNotFoundError(KeyError): + """ユーザー辞書に単語が見つからなかった。""" + + ... + +class UseUserDictError(Exception): + """OpenJTalkのユーザー辞書の設定に失敗した。""" + + ... + +class InvalidWordError(ValueError): + """ユーザー辞書の単語のバリデーションに失敗した。""" + + ... + +def _validate_pronunciation(pronunciation: str) -> None: ... +def _to_zenkaku(text: str) -> str: ... diff --git a/crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi b/crates/voicevox_core_python_api/python/voicevox_core/_rust/asyncio.pyi similarity index 80% rename from crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi rename to crates/voicevox_core_python_api/python/voicevox_core/_rust/asyncio.pyi index b09f8425f..7a6596008 100644 --- a/crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi +++ b/crates/voicevox_core_python_api/python/voicevox_core/_rust/asyncio.pyi @@ -9,26 +9,10 @@ if TYPE_CHECKING: AudioQuery, SpeakerMeta, StyleId, - SupportedDevices, - UserDict, UserDictWord, VoiceModelId, ) -__version__: str - -def supported_devices() -> SupportedDevices: - """ - このライブラリで利用可能なデバイスの情報を取得する。 - - .. code-block:: - - import voicevox_core - - supported_devices = voicevox_core.supported_devices() - """ - ... - class VoiceModel: """ 音声モデル。""" @@ -427,96 +411,3 @@ class UserDict: インポートするユーザー辞書。 """ ... - -class NotLoadedOpenjtalkDictError(Exception): - """open_jtalk辞書ファイルが読み込まれていない。""" - - ... - -class GpuSupportError(Exception): - """GPUモードがサポートされていない。""" - - ... - -class OpenZipFileError(Exception): - """ZIPファイルを開くことに失敗した。""" - - ... - -class ReadZipEntryError(Exception): - """ZIP内のファイルが読めなかった。""" - - ... - -class ModelAlreadyLoadedError(Exception): - """すでに読み込まれている音声モデルを読み込もうとした。""" - - ... - -class StyleAlreadyLoadedError(Exception): - """すでに読み込まれているスタイルを読み込もうとした。""" - - ... - -class InvalidModelDataError(Exception): - """無効なモデルデータ。""" - - ... - -class GetSupportedDevicesError(Exception): - """サポートされているデバイス情報取得に失敗した。""" - - ... - -class StyleNotFoundError(KeyError): - """スタイルIDに対するスタイルが見つからなかった。""" - - ... - -class ModelNotFoundError(KeyError): - """音声モデルIDに対する音声モデルが見つからなかった。""" - - ... - -class InferenceFailedError(Exception): - """推論に失敗した。""" - - ... - -class ExtractFullContextLabelError(Exception): - """コンテキストラベル出力に失敗した。""" - - ... - -class ParseKanaError(ValueError): - """AquesTalk風記法のテキストの解析に失敗した。""" - - ... - -class LoadUserDictError(Exception): - """ユーザー辞書を読み込めなかった。""" - - ... - -class SaveUserDictError(Exception): - """ユーザー辞書を書き込めなかった。""" - - ... - -class WordNotFoundError(KeyError): - """ユーザー辞書に単語が見つからなかった。""" - - ... - -class UseUserDictError(Exception): - """OpenJTalkのユーザー辞書の設定に失敗した。""" - - ... - -class InvalidWordError(ValueError): - """ユーザー辞書の単語のバリデーションに失敗した。""" - - ... - -def _validate_pronunciation(pronunciation: str) -> None: ... -def _to_zenkaku(text: str) -> str: ... 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..3a208fb33 --- /dev/null +++ b/crates/voicevox_core_python_api/python/voicevox_core/_rust/blocking.pyi @@ -0,0 +1,408 @@ +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 + 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。 + + 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 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: + """ユーザー辞書。""" + + @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/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/python/voicevox_core/blocking.py b/crates/voicevox_core_python_api/python/voicevox_core/blocking.py new file mode 100644 index 000000000..e378037c3 --- /dev/null +++ b/crates/voicevox_core_python_api/python/voicevox_core/blocking.py @@ -0,0 +1,3 @@ +from ._rust.blocking import OpenJtalk, Synthesizer, UserDict, VoiceModel + +__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 7e8d437ad..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() @@ -150,7 +177,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..655487148 100644 --- a/crates/voicevox_core_python_api/src/lib.rs +++ b/crates/voicevox_core_python_api/src/lib.rs @@ -2,6 +2,7 @@ use std::{marker::PhantomData, sync::Arc}; mod convert; use convert::*; +use easy_ext::ext; use log::debug; use pyo3::{ create_exception, @@ -18,7 +19,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 +27,31 @@ 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)?; - 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)?; + + 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 + 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 { @@ -338,7 +358,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, @@ -353,7 +373,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, @@ -368,7 +388,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, @@ -609,3 +629,425 @@ impl UserDict { Ok(words.into_py_dict(py)) } } + +mod blocking { + use std::sync::Arc; + + use pyo3::{ + pyclass, pymethods, + types::{IntoPyDict as _, PyBytes, PyDict, PyList}, + PyAny, PyObject, PyRef, PyResult, Python, + }; + use uuid::Uuid; + use voicevox_core::{ + AccelerationMode, AudioQueryModel, InitializeOptions, StyleId, SynthesisOptions, + TtsOptions, UserDictWord, VoiceModelId, + }; + + use crate::{convert::VoicevoxCoreResultExt as _, Closable}; + + #[pyclass] + #[derive(Clone)] + pub(crate) struct VoiceModel { + model: 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 model = voicevox_core::blocking::VoiceModel::from_path(path).into_py_result(py)?; + Ok(Self { model }) + } + + #[getter] + fn id(&self) -> &str { + 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.model.metas(), py).unwrap() + } + } + + #[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] + pub(crate) struct Synthesizer { + synthesizer: Closable< + voicevox_core::blocking::Synthesizer, + 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 { + synthesizer: Closable::new(inner), + }) + } + + fn __repr__(&self) -> &'static str { + "Synthesizer { .. }" + } + + fn __enter__(slf: PyRef<'_, Self>) -> PyResult> { + slf.synthesizer.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 synthesizer = self.synthesizer.get()?; + Ok(synthesizer.is_gpu_mode()) + } + + #[getter] + fn metas<'py>(&self, py: Python<'py>) -> PyResult> { + 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.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.synthesizer + .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 + .synthesizer + .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 synthesizer = self.synthesizer.get()?; + + let audio_query = synthesizer + .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 synthesizesr = self.synthesizer.get()?; + + let audio_query = synthesizesr + .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 synthesizer = self.synthesizer.get()?; + + let accent_phrases = synthesizer + .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 synthesizer = self.synthesizer.get()?; + + let accent_phrases = synthesizer + .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 synthesizer = self.synthesizer.get()?; + crate::convert::blocking_modify_accent_phrases( + accent_phrases, + StyleId::new(style_id), + py, + |a, s| synthesizer.replace_mora_data(&a, s), + ) + } + + fn replace_phoneme_length<'py>( + &self, + accent_phrases: &'py PyList, + style_id: u32, + py: Python<'py>, + ) -> PyResult> { + let synthesizer = self.synthesizer.get()?; + crate::convert::blocking_modify_accent_phrases( + accent_phrases, + StyleId::new(style_id), + py, + |a, s| synthesizer.replace_phoneme_length(&a, s), + ) + } + + fn replace_mora_pitch<'py>( + &self, + accent_phrases: &'py PyList, + style_id: u32, + py: Python<'py>, + ) -> PyResult> { + let synthesizer = self.synthesizer.get()?; + crate::convert::blocking_modify_accent_phrases( + accent_phrases, + StyleId::new(style_id), + py, + |a, s| synthesizer.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 + .synthesizer + .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 + .synthesizer + .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 + .synthesizer + .get()? + .tts(text, style_id, options) + .into_py_result(py)?; + Ok(PyBytes::new(py, wav)) + } + + fn close(&mut self) { + self.synthesizer.close() + } + } + + #[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", diff --git a/docs/usage.md b/docs/usage.md index e649a0949..91da1c62f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -61,28 +61,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 の初期化 @@ -94,7 +89,7 @@ asyncio.run(main()) VVM ファイルから`VoiceModel`インスタンスを作成し、`Synthesizer`に読み込ませます。その VVM ファイルにどの声が含まれているかは`VoiceModel`の`.metas`や[音声モデルと声の対応表](https://github.com/VOICEVOX/voicevox_fat_resource/blob/main/core/model/README.md#%E9%9F%B3%E5%A3%B0%E3%83%A2%E3%83%87%E3%83%ABvvm%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%A8%E5%A3%B0%E3%82%AD%E3%83%A3%E3%83%A9%E3%82%AF%E3%82%BF%E3%83%BC%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB%E5%90%8D%E3%81%A8%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB-id-%E3%81%AE%E5%AF%BE%E5%BF%9C%E8%A1%A8)で確認できます。 ```python -model = await VoiceModel.from_path("model/0.vvm") +model = VoiceModel.from_path("model/0.vvm") pprint(model.metas) ``` @@ -125,7 +120,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) ``` @@ -159,7 +154,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) ``` 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..70d204a92 --- /dev/null +++ b/example/python/run-asyncio.py @@ -0,0 +1,101 @@ +"""asyncio版のサンプルコードです。""" + +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 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 fb39715e9..a57139b1c 100644 --- a/example/python/run.py +++ b/example/python/run.py @@ -1,4 +1,3 @@ -import asyncio import dataclasses import json import logging @@ -7,17 +6,11 @@ from typing import Tuple import voicevox_core -from voicevox_core import ( - AccelerationMode, - AudioQuery, - OpenJtalk, - Synthesizer, - VoiceModel, -) +from voicevox_core import AccelerationMode, AudioQuery +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") @@ -37,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}`") @@ -102,4 +95,4 @@ def display_as_json(audio_query: AudioQuery) -> str: if __name__ == "__main__": - asyncio.run(main()) + main()