Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PythonのブロッキングAPIを実装 #706

Merged
merged 13 commits into from
Dec 9, 2023
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
Copy link
Member Author

@qryxip qryxip Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PRが出ているが、以下のコメントから約2ヶ月放置されている状態になっている。
readthedocs/sphinx-autoapi#406 (comment)

Copy link
Member Author

@qryxip qryxip Dec 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

あと、このworkaroundを実行するとコードとしては壊れて実行できなくなる(_rust.abi3.*よりも_rust/__init__.pyの方が優先されるらしい)。

Sphinxを騙すためだけにGHA上で実行。

Copy link
Member Author

@qryxip qryxip Dec 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

手元で生成するのに面倒が生じてきてるので、いっそのこと他言語ごと

cargo xtask doc [--only rust c python ...] [--open]

みたいな感じで生成できるようにした方がよいかもしれない。

- 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
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,14 @@
supported_devices,
)

from . import blocking # noqa: F401 isort: skip

__all__ = [
"__version__",
"AccelerationMode",
"AccentPhrase",
"AudioQuery",
"blocking",
"ExtractFullContextLabelError",
"GetSupportedDevicesError",
"GpuSupportError",
Expand Down
Original file line number Diff line number Diff line change
@@ -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
インポートするユーザー辞書。
"""
...
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ._rust.blocking import OpenJtalk, UserDict

__all__ = ["OpenJtalk", "UserDict"]
2 changes: 1 addition & 1 deletion crates/voicevox_core_python_api/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ pub fn to_rust_word_type(word_type: &PyAny) -> PyResult<UserDictWordType> {
serde_json::from_value::<UserDictWordType>(json!(name)).into_py_value_result()
}

#[ext]
#[ext(VoicevoxCoreResultExt)]
pub impl<T> voicevox_core::Result<T> {
fn into_py_result(self, py: Python<'_>) -> PyResult<T> {
use voicevox_core::ErrorKind;
Expand Down
136 changes: 128 additions & 8 deletions crates/voicevox_core_python_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -18,20 +18,31 @@ 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"))?;
module.add_wrapped(wrap_pyfunction!(supported_devices))?;
module.add_wrapped(wrap_pyfunction!(_validate_pronunciation))?;
module.add_wrapped(wrap_pyfunction!(_to_zenkaku))?;

module.add_class::<Synthesizer>()?;
module.add_class::<OpenJtalk>()?;
module.add_class::<VoiceModel>()?;
module.add_class::<UserDict>()?;

add_exceptions(module)
module.add_class::<self::Synthesizer>()?;
module.add_class::<self::OpenJtalk>()?;
module.add_class::<self::VoiceModel>()?;
module.add_class::<self::UserDict>()?;

add_exceptions(module)?;

let blocking_module = PyModule::new(py, "voicevox_core._rust.blocking")?;
blocking_module.add_class::<self::blocking::OpenJtalk>()?;
blocking_module.add_class::<self::blocking::UserDict>()?;
// 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 {
Expand Down Expand Up @@ -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<Self> {
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<voicevox_core::blocking::UserDict>,
}

#[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<PyObject> {
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::<PyResult<Vec<_>>>()
})?;
Ok(words.into_py_dict(py))
}
}
}
1 change: 1 addition & 0 deletions docs/apis/python_api/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading