Skip to content

Commit

Permalink
PythonのブロッキングAPIを実装 (#706)
Browse files Browse the repository at this point in the history
* `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のコメント変更
  • Loading branch information
qryxip authored Dec 9, 2023
1 parent df9f9f6 commit 00a1c53
Show file tree
Hide file tree
Showing 22 changed files with 1,341 additions and 182 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/generate_document.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/python_lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
6 changes: 5 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# ユーザー辞書の単語が反映されるかをテストする。
"""
ユーザー辞書の単語が反映されるかをテストする。
``test_pseudo_raii_for_blocking_synthesizer`` と対になる。
"""

# AudioQueryのkanaを比較して変化するかどうかで判断する。

from uuid import UUID
Expand All @@ -10,17 +15,17 @@

@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)

audio_query_without_dict = await synthesizer.audio_query(
"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",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# ユーザー辞書の操作をテストする。
"""
ユーザー辞書の操作をテストする。
``test_blocking_user_dict_manipulate`` と対になる。
"""

# どのコードがどの操作を行っているかはコメントを参照。

import os
Expand All @@ -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(
Expand All @@ -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",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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="カタカナ以外の文字",
)
)
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
12 changes: 4 additions & 8 deletions crates/voicevox_core_python_api/python/voicevox_core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -57,7 +57,6 @@
"ModelNotFoundError",
"Mora",
"NotLoadedOpenjtalkDictError",
"OpenJtalk",
"OpenZipFileError",
"ParseKanaError",
"ReadZipEntryError",
Expand All @@ -68,11 +67,8 @@
"StyleNotFoundError",
"StyleVersion",
"SupportedDevices",
"Synthesizer",
"VoiceModel",
"supported_devices",
"UseUserDictError",
"UserDict",
"UserDictWord",
"UserDictWordType",
"VoiceModelId",
Expand Down
Loading

0 comments on commit 00a1c53

Please sign in to comment.