diff --git a/.github/workflows/generate_document.yml b/.github/workflows/generate_document.yml index 40fc29c24..bdf71905a 100644 --- a/.github/workflows/generate_document.yml +++ b/.github/workflows/generate_document.yml @@ -20,6 +20,11 @@ jobs: uses: actions/setup-python@v4 with: python-version: "3.8" + - name: Setup Java + uses: actions/setup-java@v2 + with: + java-version: "11" + distribution: "adopt" - name: Install cargo-binstall uses: taiki-e/install-action@cargo-binstall - name: Create a venv @@ -42,8 +47,14 @@ jobs: maturin develop --manifest-path ./crates/voicevox_core_python_api/Cargo.toml --locked - name: Generate Sphinx document run: sphinx-build docs/apis/python_api public/apis/python_api + - name: Generate Javadoc + run: | + (cd crates/voicevox_core_java_api && ./gradlew javadoc) + mkdir -p public/apis/java_api + cp -r crates/voicevox_core_java_api/lib/build/docs/javadoc/* public/apis/java_api - name: Uplaod api document uses: actions/upload-pages-artifact@v1 + if: ${{ github.ref_name == 'main' }} with: path: public deploy_api_github_pages: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e575974bc..4adb3b9bf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -283,3 +283,51 @@ jobs: pip install -r requirements-test.txt pytest + build-and-test-java-api: + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + - os: macos-latest + - os: ubuntu-latest + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + - name: Set up Rust + uses: ./.github/actions/rust-toolchain-from-file + - name: Set up Java + uses: actions/setup-java@v2 + with: + java-version: "11" + distribution: "adopt" + - name: Build + run: | + cargo build -p voicevox_core_java_api -vv + cargo build -p test_util -vv + - name: 必要なDLLをコピーしてテストを実行 + working-directory: crates/voicevox_core_java_api + run: | + OS=$(tr '[:upper:]' '[:lower:]' <<<"$RUNNER_OS") + ARCH=$(tr '[:upper:]' '[:lower:]' <<<"$RUNNER_ARCH") + + case "$RUNNER_OS" in + Windows) + DLL_NAME="voicevox_core_java_api.dll" + ;; + macOS) + DLL_NAME="libvoicevox_core_java_api.dylib" + ;; + Linux) + DLL_NAME="libvoicevox_core_java_api.so" + ;; + *) + echo "Unsupported OS: $RUNNER_OS" + exit 1 + ;; + esac + TARGET_NAME="$OS-$ARCH" + mkdir -p "./lib/src/main/resources/dll/$TARGET_NAME" + cp -v "../../target/debug/$DLL_NAME" "./lib/src/main/resources/dll/$TARGET_NAME/$DLL_NAME" + echo "target = $TARGET_NAME, dll = $DLL_NAME" + ./gradlew test --info diff --git a/Cargo.lock b/Cargo.lock index a4460dae4..9d612c4fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,6 +92,30 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_log-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f0fc03f560e1aebde41c2398b691cb98b5ea5996a6184a7a67bbbb77448969" + +[[package]] +name = "android_logger" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa490e751f3878eb9accb9f18988eca52c2337ce000a8bf31ef50d4c723ca9e" +dependencies = [ + "android_log-sys", + "env_logger 0.10.0", + "log", + "once_cell", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -356,7 +380,7 @@ dependencies = [ "cexpr", "clang-sys", "clap 3.2.22", - "env_logger", + "env_logger 0.9.1", "lazy_static", "lazycell", "log", @@ -509,6 +533,12 @@ dependencies = [ "jobserver", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cexpr" version = "0.6.0" @@ -526,14 +556,17 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.23" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ + "android-tzdata", "iana-time-zone", - "num-integer", + "js-sys", "num-traits", "serde", + "time 0.1.45", + "wasm-bindgen", "winapi", ] @@ -679,6 +712,16 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes 1.1.0", + "memchr", +] + [[package]] name = "concolor" version = "0.0.11" @@ -1098,6 +1141,16 @@ dependencies = [ "termcolor", ] +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "log", + "regex", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1799,6 +1852,28 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.25" @@ -2995,6 +3070,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.20" @@ -3535,6 +3619,17 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "time" version = "0.2.27" @@ -3775,9 +3870,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" dependencies = [ "matchers", "nu-ansi-term", @@ -4027,6 +4122,23 @@ dependencies = [ "voicevox_core", ] +[[package]] +name = "voicevox_core_java_api" +version = "0.0.0" +dependencies = [ + "android_logger", + "anyhow", + "chrono", + "jni", + "once_cell", + "serde_json", + "test_util", + "tokio", + "tracing", + "tracing-subscriber", + "voicevox_core", +] + [[package]] name = "voicevox_core_python_api" version = "0.0.0" @@ -4061,6 +4173,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +[[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.0" @@ -4077,6 +4199,12 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -4235,13 +4363,13 @@ version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" dependencies = [ - "windows_aarch64_gnullvm 0.42.0", - "windows_aarch64_msvc 0.42.0", - "windows_i686_gnu 0.42.0", - "windows_i686_msvc 0.42.0", - "windows_x86_64_gnu 0.42.0", - "windows_x86_64_gnullvm 0.42.0", - "windows_x86_64_msvc 0.42.0", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -4263,13 +4391,22 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm 0.42.0", - "windows_aarch64_msvc 0.42.0", - "windows_i686_gnu 0.42.0", - "windows_i686_msvc 0.42.0", - "windows_x86_64_gnu 0.42.0", - "windows_x86_64_gnullvm 0.42.0", - "windows_x86_64_msvc 0.42.0", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", ] [[package]] @@ -4278,7 +4415,22 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -4298,9 +4450,9 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" @@ -4316,9 +4468,9 @@ checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" [[package]] name = "windows_aarch64_msvc" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" @@ -4334,9 +4486,9 @@ checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" [[package]] name = "windows_i686_gnu" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" @@ -4352,9 +4504,9 @@ checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" @@ -4370,9 +4522,9 @@ checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" [[package]] name = "windows_x86_64_gnu" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" @@ -4382,9 +4534,9 @@ checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" @@ -4400,9 +4552,9 @@ checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" [[package]] name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" diff --git a/Cargo.toml b/Cargo.toml index 3e66cff9f..287e7e854 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/test_util", "crates/voicevox_core", "crates/voicevox_core_c_api", + "crates/voicevox_core_java_api", "crates/voicevox_core_python_api", "crates/xtask" ] diff --git a/crates/voicevox_core_java_api/.gitattributes b/crates/voicevox_core_java_api/.gitattributes new file mode 100644 index 000000000..493fdc40f --- /dev/null +++ b/crates/voicevox_core_java_api/.gitattributes @@ -0,0 +1,12 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +./gradlew text eol=lf linguist-vendored linguist-generated + +# These are Windows script files and should use crlf +*.bat text eol=crlf + +./gradlew linguist-vendored linguist-generated +./gradlew.bat linguist-vendored linguist-generated +./gradle/wrapper/gradle-wrapper.jar linguist-vendored linguist-generated diff --git a/crates/voicevox_core_java_api/.gitignore b/crates/voicevox_core_java_api/.gitignore new file mode 100644 index 000000000..0c7fb6035 --- /dev/null +++ b/crates/voicevox_core_java_api/.gitignore @@ -0,0 +1,12 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build + +!lib + +lib/src/main/resources/dll/*/* +!lib/src/main/resources/dll/*/.gitkeep +lib/src/main/resources/jniLibs/*/* +!lib/src/main/resources/jniLibs/*/.gitkeep diff --git a/crates/voicevox_core_java_api/Cargo.toml b/crates/voicevox_core_java_api/Cargo.toml new file mode 100644 index 000000000..7a8583245 --- /dev/null +++ b/crates/voicevox_core_java_api/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "voicevox_core_java_api" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[lib] +crate-type = ["cdylib"] + +[dependencies] +android_logger = "0.13.1" +anyhow.workspace = true +chrono = "0.4.26" +jni = "0.21.1" +once_cell.workspace = true +serde_json.workspace = true +tokio.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +voicevox_core.workspace = true + +[dev-dependencies] +test_util.workspace = true diff --git a/crates/voicevox_core_java_api/README.md b/crates/voicevox_core_java_api/README.md new file mode 100644 index 000000000..d1bc31938 --- /dev/null +++ b/crates/voicevox_core_java_api/README.md @@ -0,0 +1,96 @@ +# voicevox_core_java_api + +VOICEVOX CORE の Java バインディング。 + +## 環境構築 + +以下の環境が必要です: + +- Rustup +- JDK 11 + +## ファイル構成 + +```yml +- README.md +- Cargo.toml # Rustプロジェクトとしてのマニフェストファイル。 +- lib: + - build.gradle # Gradle(Java)プロジェクトとしてのマニフェストファイル。 + - src: + - main: + - java: + - jp/hiroshiba/voicevoxcore: # Javaのソースコード。 + - Synthesizer.java + - ... + - resources: + - dll: # ライブラリ用のディレクトリ。詳細は後述。 + - windows-x64: + - voicevox_core.dll + - ... + - jniLibs: # Android用のディレクトリ。 + - x86_64: + - ... +- src: # Rustのソースコード。jni-rsを使ってJavaとのバインディングを行う。 + - lib.rs + - ... +``` + +## ビルド(開発) + +バインディングは `cargo build` でビルドできます。 +Java プロジェクトを動かすには、 + +- `LD_LIBRARY_PATH`などの環境変数に `[プロジェクトルート]/target/debug`(または`/release`) を追加するか、 +- `lib/src/main/resources/dll/[target]/libvoicevox_core_java_api.so` を作成する(`libvoicevox_core_java_api.so`はプラットフォームによって異なります、詳細は後述)。 + +必要があります。 + +```console +❯ cargo build +❯ LD_LIBRARY_PATH=$(realpath ../../target/debug) ./gradlew build + +# または +❯ cp ../../target/debug/libvoicevox_core_java_api.so lib/src/main/resources/dll/[target]/libvoicevox_core_java_api.so +❯ ./gradlew build +``` + +## ビルド(リリース) + +`cargo build --release` で Rust 側を、`./gradlew build` で Java 側をビルドできます。 +パッケージ化する時は lib/src/main/resources/dll 内に dll をコピーしてください。 + +```console +❯ cargo build --release +❯ cp ../../target/release/libvoicevox_core_java_api.so lib/src/main/resources/dll/[target]/libvoicevox_core_java_api.so +❯ ./gradlew build +``` + +## テスト + +`./gradlew test` でテストできます。 + +```console +❯ ./gradlew test +``` + +## ドキュメント + +`./gradlew javadoc` でドキュメントを生成できます。 + +```console +❯ ./gradlew javadoc +``` + +## DLL 読み込みについて + +Android では、jniLibs から System.loadLibrary で読み込みます。 + +Android 以外では、src/main/resources/dll 内の適切な DLL を一時ディレクトリにコピーし、System.load で読み込みます。 +DLL の名前は、 + +- Windows:voicevox_core_java_api.dll +- Linux:libvoicevox_core_java_api.so +- macOS:libvoicevox_core_java_api.dylib + +になります。 +見付からなかった場合は、`System.loadLibrary` で読み込みます。これはデバッグ用です。 diff --git a/crates/voicevox_core_java_api/gradle/wrapper/gradle-wrapper.jar b/crates/voicevox_core_java_api/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..033e24c4c Binary files /dev/null and b/crates/voicevox_core_java_api/gradle/wrapper/gradle-wrapper.jar differ diff --git a/crates/voicevox_core_java_api/gradle/wrapper/gradle-wrapper.properties b/crates/voicevox_core_java_api/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..9f4197d5f --- /dev/null +++ b/crates/voicevox_core_java_api/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/crates/voicevox_core_java_api/gradlew b/crates/voicevox_core_java_api/gradlew new file mode 100755 index 000000000..fcb6fca14 --- /dev/null +++ b/crates/voicevox_core_java_api/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/crates/voicevox_core_java_api/gradlew.bat b/crates/voicevox_core_java_api/gradlew.bat new file mode 100644 index 000000000..93e3f59f1 --- /dev/null +++ b/crates/voicevox_core_java_api/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/crates/voicevox_core_java_api/lib/build.gradle b/crates/voicevox_core_java_api/lib/build.gradle new file mode 100644 index 000000000..3e87cceff --- /dev/null +++ b/crates/voicevox_core_java_api/lib/build.gradle @@ -0,0 +1,61 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Java library project to get you started. + * For more details on building Java & JVM projects, please refer to https://docs.gradle.org/8.2.1/userguide/building_java_projects.html in the Gradle documentation. + */ + +plugins { + // Apply the java-library plugin for API and implementation separation. + id 'java-library' + id "com.diffplug.spotless" version "6.20.0" +} + +version = '0.0.0' + +repositories { + // Use Maven Central for resolving dependencies. + mavenCentral() +} + +dependencies { + // Use JUnit Jupiter for testing. + testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' + + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // This dependency is exported to consumers, that is to say found on their compile classpath. + api 'org.apache.commons:commons-math3:3.6.1' + + // This dependency is used internally, and not exposed to consumers on their own compile classpath. + implementation 'com.google.guava:guava:31.1-jre' + + // https://mvnrepository.com/artifact/com.google.code.gson/gson + implementation group: 'com.google.code.gson', name: 'gson', version: '2.10.1' + + // https://mvnrepository.com/artifact/jakarta.validation/jakarta.validation-api + implementation group: 'jakarta.validation', name: 'jakarta.validation-api', version: '3.0.2' + + implementation group: 'com.microsoft.onnxruntime', name: 'onnxruntime', version: '1.14.0' +} + +// Apply a specific Java toolchain to ease working on different environments. +java { + toolchain { + languageVersion = JavaLanguageVersion.of(11) + } +} + +tasks.named('test') { + // Use JUnit Platform for unit tests. + useJUnitPlatform() +} + +compileJava.options.encoding = 'UTF-8' +compileTestJava.options.encoding = 'UTF-8' + +spotless { + java { + googleJavaFormat() + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/AccentPhrase.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/AccentPhrase.java new file mode 100644 index 000000000..8c64c9d11 --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/AccentPhrase.java @@ -0,0 +1,40 @@ +package jp.hiroshiba.voicevoxcore; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** AccentPhrase (アクセント句ごとの情報)。 */ +public class AccentPhrase { + /** モーラの配列。 */ + @SerializedName("moras") + @Expose + @Nonnull + public List moras; + + /** アクセント箇所。 */ + @SerializedName("accent") + @Expose + public int accent; + + /** 後ろに無音を付けるかどうか。 */ + @SerializedName("pause_mora") + @Expose + @Nullable + public Mora pauseMora; + + /** 疑問系かどうか。 */ + @SerializedName("is_interrogative") + @Expose + public boolean isInterrogative; + + public AccentPhrase() { + this.moras = new ArrayList<>(); + this.accent = 0; + this.pauseMora = null; + this.isInterrogative = false; + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/AudioQuery.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/AudioQuery.java new file mode 100644 index 000000000..4a23be3f7 --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/AudioQuery.java @@ -0,0 +1,79 @@ +package jp.hiroshiba.voicevoxcore; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** AudioQuery(音声合成用のクエリ)。 */ +public class AudioQuery { + /** アクセント句の配列。 */ + @SerializedName("accent_phrases") + @Expose + @Nonnull + public List accentPhrases; + + /** 全体の話速。 */ + @SerializedName("speed_scale") + @Expose + public double speedScale; + + /** 全体の音高。 */ + @SerializedName("pitch_scale") + @Expose + public double pitchScale; + + /** 全体の抑揚。 */ + @SerializedName("intonation_scale") + @Expose + public double intonationScale; + + /** 全体の音量。 */ + @SerializedName("volume_scale") + @Expose + public double volumeScale; + + /** 音声の前の無音時間。 */ + @SerializedName("pre_phoneme_length") + @Expose + public double prePhonemeLength; + + /** 音声の後の無音時間。 */ + @SerializedName("post_phoneme_length") + @Expose + public double postPhonemeLength; + + /** 音声データの出力サンプリングレート。 */ + @SerializedName("output_sampling_rate") + @Expose + public int outputSamplingRate; + + /** 音声データをステレオ出力するか否か。 */ + @SerializedName("output_stereo") + @Expose + public boolean outputStereo; + + /** + * [読み取り専用] AquesTalk風記法。 + * + *

{@link Synthesizer#createAudioQuery} が返すもののみ String となる。入力としてのAudioQueryでは無視される。 + */ + @SerializedName("kana") + @Expose + @Nullable + public final String kana; + + public AudioQuery() { + this.accentPhrases = new ArrayList<>(); + this.speedScale = 1.0; + this.pitchScale = 0.0; + this.intonationScale = 1.0; + this.volumeScale = 1.0; + this.prePhonemeLength = 0.1; + this.postPhonemeLength = 0.1; + this.outputSamplingRate = 24000; + this.kana = null; + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/Dll.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/Dll.java new file mode 100644 index 000000000..e4e674205 --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/Dll.java @@ -0,0 +1,68 @@ +package jp.hiroshiba.voicevoxcore; + +import ai.onnxruntime.OrtEnvironment; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** ライブラリを読み込むためだけのクラス。 */ +abstract class Dll { + static { + String runtimeName = System.getProperty("java.runtime.name"); + if (runtimeName.equals("Android Runtime")) { + // Android ではjniLibsから読み込む。 + System.loadLibrary("voicevox_core_java_api"); + } else { + String rawOsName = System.getProperty("os.name"); + String rawOsArch = System.getProperty("os.arch"); + String osName, osArch, dllName; + if (rawOsName.startsWith("Win")) { + osName = "windows"; + dllName = "voicevox_core_java_api.dll"; + } else if (rawOsName.startsWith("Mac")) { + osName = "macos"; + dllName = "libvoicevox_core_java_api.dylib"; + } else if (rawOsName.startsWith("Linux")) { + osName = "linux"; + dllName = "libvoicevox_core_java_api.so"; + } else { + throw new RuntimeException("Unsupported OS: " + rawOsName); + } + if (rawOsArch.equals("x86")) { + osArch = "x86"; + } else if (rawOsArch.equals("x86_64")) { + osArch = "x64"; + } else if (rawOsArch.equals("amd64")) { + osArch = "x64"; + } else if (rawOsArch.equals("aarch64")) { + osArch = "arm64"; + } else { + throw new RuntimeException("Unsupported OS architecture: " + rawOsArch); + } + + String target = osName + "-" + osArch; + // ONNX Runtime の DLL を読み込む。 + OrtEnvironment.getEnvironment(); + try (InputStream in = Dll.class.getResourceAsStream("/dll/" + target + "/" + dllName)) { + if (in == null) { + try { + // フォールバック。開発用。 + System.loadLibrary("voicevox_core_java_api"); + } catch (UnsatisfiedLinkError e) { + throw new RuntimeException("Failed to load Voicevox Core DLL for " + target, e); + } + } else { + Path tempDir = Paths.get(System.getProperty("java.io.tmpdir")); + Path dllPath = tempDir.resolve(dllName); + dllPath.toFile().deleteOnExit(); + Files.copy(in, dllPath); + + System.load(dllPath.toAbsolutePath().toString()); + } + } catch (Exception e) { + throw new RuntimeException("Failed to load Voicevox Core DLL for " + target, e); + } + } + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/Mora.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/Mora.java new file mode 100644 index 000000000..61dbaf233 --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/Mora.java @@ -0,0 +1,53 @@ +package jp.hiroshiba.voicevoxcore; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** モーラ(子音+母音)ごとの情報。 */ +public class Mora { + /** 文字。 */ + @SerializedName("text") + @Expose + @Nonnull + @SuppressWarnings("NullableProblems") + public String text; + + /** 子音の音素。 */ + @SerializedName("consonant") + @Expose + @Nullable + public String consonant; + + /** 子音の音長。 */ + @SerializedName("consonant_length") + @Expose + public java.lang.Double consonantLength; + + /** 母音の音素。 */ + @SerializedName("vowel") + @Expose + @Nonnull + @SuppressWarnings("NullableProblems") + public String vowel; + + /** 母音の音長。 */ + @SerializedName("vowel_length") + @Expose + public double vowelLength; + + /** 音高。 */ + @SerializedName("pitch") + @Expose + public double pitch; + + public Mora() { + this.text = ""; + this.consonant = null; + this.consonantLength = null; + this.vowel = ""; + this.vowelLength = 0.0; + this.pitch = 0.0; + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/OpenJtalk.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/OpenJtalk.java new file mode 100644 index 000000000..3228b8d51 --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/OpenJtalk.java @@ -0,0 +1,39 @@ +package jp.hiroshiba.voicevoxcore; + +import java.lang.ref.Cleaner; + +/** テキスト解析器としてのOpen JTalk。 */ +public class OpenJtalk extends Dll { + private long handle; + private static final Cleaner cleaner = Cleaner.create(); + + /** + * Open JTalkの辞書ディレクトリ。 + * + * @param openJtalkDictDir 辞書のディレクトリ。 + */ + public OpenJtalk(String openJtalkDictDir) { + rsNewWithInitialize(openJtalkDictDir); + + cleaner.register(this, () -> rsDrop()); + } + + /** + * ユーザー辞書を設定する。 + * + *

この関数を呼び出した後にユーザー辞書を変更した場合は、再度この関数を呼ぶ必要がある。 + * + * @param userDict ユーザー辞書。 + */ + public void useUserDict(UserDict userDict) { + rsUseUserDict(userDict); + } + + private native void rsNewWithoutDic(); + + private native void rsNewWithInitialize(String openJtalkDictDir); + + private native void rsUseUserDict(UserDict userDict); + + private native void rsDrop(); +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/Synthesizer.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/Synthesizer.java new file mode 100644 index 000000000..2e97cc59c --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/Synthesizer.java @@ -0,0 +1,473 @@ +package jp.hiroshiba.voicevoxcore; + +import com.google.gson.Gson; +import java.lang.ref.Cleaner; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.annotation.Nonnull; + +/** + * 音声シンセサイザ。 + * + * @see Synthesizer#builder + */ +public class Synthesizer extends Dll { + private long handle; + private static final Cleaner cleaner = Cleaner.create(); + + private Synthesizer(OpenJtalk openJtalk, Builder builder) { + rsNewWithInitialize(openJtalk, builder); + cleaner.register(this, () -> rsDrop()); + } + + /** + * モデルを読み込む。 + * + * @param voiceModel 読み込むモデル。 + */ + public void loadVoiceModel(VoiceModel voiceModel) { + rsLoadVoiceModel(voiceModel); + } + + /** + * 音声モデルの読み込みを解除する。 + * + * @param voiceModelId 読み込みを解除する音声モデルのID。 + */ + public void unloadVoiceModel(String voiceModelId) { + rsUnloadVoiceModel(voiceModelId); + } + + /** + * 指定した音声モデルのIDが読み込まれているかどうかを返す。 + * + * @param voiceModelId 音声モデルのID。 + * @return 指定した音声モデルのIDが読み込まれているかどうか。 + */ + public boolean isLoadedVoiceModel(String voiceModelId) { + return rsIsLoadedVoiceModel(voiceModelId); + } + + /** + * {@link AudioQuery} を生成するためのオブジェクトを生成する。 + * + * @param text テキスト。 + * @param styleId スタイルID。 + * @return {@link CreateAudioQueryConfigurator}。 + * + * @see CreateAudioQueryConfigurator#execute + */ + @Nonnull + public CreateAudioQueryConfigurator createAudioQuery(String text, int styleId) { + return new CreateAudioQueryConfigurator(this, text, styleId); + } + + /** + * {@link AccentPhrase} のリストを生成するためのオブジェクトを生成する。 + * + * @param text テキスト。 + * @param styleId スタイルID。 + * @return {@link CreateAccentPhrasesConfigurator}。 + * + * @see CreateAccentPhrasesConfigurator#execute + */ + @Nonnull + public CreateAccentPhrasesConfigurator createAccentPhrases(String text, int styleId) { + return new CreateAccentPhrasesConfigurator(this, text, styleId); + } + + /** + * アクセント句の音高・音素長を変更する。 + * + * @param accentPhrases 変更元のアクセント句の配列。 + * @param styleId スタイルID。 + * @return 変更後のアクセント句の配列。 + */ + @Nonnull + public List replaceMoraData(List accentPhrases, int styleId) { + if (!Utils.isU32(styleId)) { + throw new IllegalArgumentException("styleId"); + } + String accentPhrasesJson = new Gson().toJson(accentPhrases); + String replacedAccentPhrasesJson = rsReplaceMoraData(accentPhrasesJson, styleId, false); + return new ArrayList<>( + Arrays.asList(new Gson().fromJson(replacedAccentPhrasesJson, AccentPhrase[].class))); + } + + /** + * アクセント句の音素長を変更する。 + * + * @param accentPhrases 変更元のアクセント句の配列。 + * @param styleId スタイルID。 + * @return 変更後のアクセント句の配列。 + */ + @Nonnull + public List replacePhonemeLength(List accentPhrases, int styleId) { + if (!Utils.isU32(styleId)) { + throw new IllegalArgumentException("styleId"); + } + String accentPhrasesJson = new Gson().toJson(accentPhrases); + String replacedAccentPhrasesJson = rsReplacePhonemeLength(accentPhrasesJson, styleId, false); + return new ArrayList<>( + Arrays.asList(new Gson().fromJson(replacedAccentPhrasesJson, AccentPhrase[].class))); + } + + /** + * アクセント句の音高を変更する。 + * + * @param accentPhrases 変更元のアクセント句の配列。 + * @param styleId スタイルID。 + * @return 変更後のアクセント句の配列。 + */ + @Nonnull + public List replaceMoraPitch(List accentPhrases, int styleId) { + if (!Utils.isU32(styleId)) { + throw new IllegalArgumentException("styleId"); + } + String accentPhrasesJson = new Gson().toJson(accentPhrases); + String replacedAccentPhrasesJson = rsReplaceMoraPitch(accentPhrasesJson, styleId, false); + return new ArrayList<>( + Arrays.asList(new Gson().fromJson(replacedAccentPhrasesJson, AccentPhrase[].class))); + } + + /** + * {@link AudioQuery} から音声合成するためのオブジェクトを生成する。 + * + * @param audioQuery {@link AudioQuery}。 + * @param styleId スタイルID。 + * @return {@link SynthesisConfigurator}。 + * + * @see SynthesisConfigurator#execute + */ + @Nonnull + public SynthesisConfigurator synthesis(AudioQuery audioQuery, int styleId) { + return new SynthesisConfigurator(this, audioQuery, styleId); + } + + /** + * テキスト音声合成を実行するためのオブジェクトを生成する。 + * + * @param text テキスト。 + * @param styleId スタイルID。 + * @return {@link TtsConfigurator}。 + * + * @see TtsConfigurator#execute + */ + @Nonnull + public TtsConfigurator tts(String text, int styleId) { + return new TtsConfigurator(this, text, styleId); + } + + private native void rsNewWithInitialize(OpenJtalk openJtalk, Builder builder); + + private native void rsLoadVoiceModel(VoiceModel voiceModel); + + private native void rsUnloadVoiceModel(String voiceModelId); + + private native boolean rsIsLoadedVoiceModel(String voiceModelId); + + @Nonnull + private native String rsAudioQuery(String text, int styleId, boolean kana); + + @Nonnull + private native String rsAccentPhrases(String text, int styleId, boolean kana); + + @Nonnull + private native String rsReplaceMoraData(String accentPhrasesJson, int styleId, boolean kana); + + @Nonnull + private native String rsReplacePhonemeLength(String accentPhrasesJson, int styleId, boolean kana); + + @Nonnull + private native String rsReplaceMoraPitch(String accentPhrasesJson, int styleId, boolean kana); + + @Nonnull + private native byte[] rsSynthesis( + String queryJson, int styleId, boolean enableInterrogativeUpspeak); + + @Nonnull + private native byte[] rsTts( + String text, int styleId, boolean kana, boolean enableInterrogativeUpspeak); + + private native void rsDrop(); + + public static Builder builder(OpenJtalk openJtalk) { + return new Builder(openJtalk); + } + + /** + * 音声シンセサイザのビルダー。 + * + * @see Synthesizer#builder + */ + public static class Builder { + private OpenJtalk openJtalk; + + @SuppressWarnings("unused") + private AccelerationMode accelerationMode; + + @SuppressWarnings("unused") + private int cpuNumThreads; + + @SuppressWarnings("unused") + private boolean loadAllModels; + + public Builder(OpenJtalk openJtalk) { + this.openJtalk = openJtalk; + } + + /** + * ハードウェアアクセラレーションモードを設定する。 + * + * @param accelerationMode ハードウェアアクセラレーションモード。 + * @return ビルダー。 + */ + public Builder accelerationMode(AccelerationMode accelerationMode) { + this.accelerationMode = accelerationMode; + return this; + } + + /** + * CPU利用数を指定する。0を指定すると環境に合わせたCPUが利用される。 + * + * @param cpuNumThreads CPU利用数。 + * @return ビルダー。 + */ + public Builder cpuNumThreads(int cpuNumThreads) { + if (Utils.isU16(cpuNumThreads)) { + throw new IllegalArgumentException("cpuNumThreads"); + } + this.cpuNumThreads = cpuNumThreads; + return this; + } + + /** + * 全てのモデルを読み込むかどうか。 + * + * @param loadAllModels 全てのモデルを読み込むかどうか。 + * @return ビルダー。 + */ + public Builder loadAllModels(boolean loadAllModels) { + this.loadAllModels = loadAllModels; + return this; + } + + /** + * {@link Synthesizer} を構築する。 + * + * @return {@link Synthesizer}。 + */ + public Synthesizer build() { + Synthesizer synthesizer = new Synthesizer(openJtalk, this); + return synthesizer; + } + } + + /** ハードウェアアクセラレーションモード。 */ + public static enum AccelerationMode { + /** 実行環境に合わせて自動的に選択する。 */ + AUTO, + /** CPUに設定する。 */ + CPU, + /** GPUに設定する。 */ + GPU, + } + + /** {@link Synthesizer#createAudioQuery} のオプション。 */ + public class CreateAudioQueryConfigurator { + private Synthesizer synthesizer; + private String text; + private int styleId; + private boolean kana; + + private CreateAudioQueryConfigurator(Synthesizer synthesizer, String text, int styleId) { + if (!Utils.isU32(styleId)) { + throw new IllegalArgumentException("styleId"); + } + this.synthesizer = synthesizer; + this.text = text; + this.styleId = styleId; + this.kana = false; + } + + /** + * 入力テキストをAquesTalk風記法として解釈するかどうか。 + * + * @param kana 入力テキストをAquesTalk風記法として解釈するかどうか。 + * @return {@link CreateAudioQueryConfigurator}。 + */ + @Nonnull + public CreateAudioQueryConfigurator kana(boolean kana) { + this.kana = kana; + return this; + } + + /** + * {@link AudioQuery} を生成する。 + * + * @return {@link AudioQuery}。 + */ + @Nonnull + public AudioQuery execute() { + if (!Utils.isU32(styleId)) { + throw new IllegalArgumentException("styleId"); + } + String queryJson = synthesizer.rsAudioQuery(this.text, this.styleId, this.kana); + Gson gson = new Gson(); + + AudioQuery audioQuery = gson.fromJson(queryJson, AudioQuery.class); + if (audioQuery == null) { + throw new NullPointerException("audio_query"); + } + return audioQuery; + } + } + + /** {@link Synthesizer#createAccentPhrases} のオプション。 */ + public class CreateAccentPhrasesConfigurator { + private Synthesizer synthesizer; + private String text; + private int styleId; + private boolean kana; + + private CreateAccentPhrasesConfigurator(Synthesizer synthesizer, String text, int styleId) { + if (!Utils.isU32(styleId)) { + throw new IllegalArgumentException("styleId"); + } + this.synthesizer = synthesizer; + this.text = text; + this.styleId = styleId; + this.kana = false; + } + + /** + * 入力テキストをAquesTalk風記法として解釈するかどうか。 + * + * @param kana 入力テキストをAquesTalk風記法として解釈するかどうか。 + * @return {@link CreateAudioQueryConfigurator}。 + */ + @Nonnull + public CreateAccentPhrasesConfigurator kana(boolean kana) { + this.kana = kana; + return this; + } + + /** + * {@link AccentPhrase} のリストを取得する。 + * + * @return {@link AccentPhrase} のリスト。 + */ + @Nonnull + public List execute() { + String accentPhrasesJson = synthesizer.rsAccentPhrases(this.text, this.styleId, this.kana); + Gson gson = new Gson(); + AccentPhrase[] rawAccentPhrases = gson.fromJson(accentPhrasesJson, AccentPhrase[].class); + if (rawAccentPhrases == null) { + throw new NullPointerException("accent_phrases"); + } + return new ArrayList(Arrays.asList(rawAccentPhrases)); + } + } + + /** {@link Synthesizer#synthesis} のオプション。 */ + public class SynthesisConfigurator { + private Synthesizer synthesizer; + private AudioQuery audioQuery; + private int styleId; + private boolean interrogativeUpspeak; + + private SynthesisConfigurator(Synthesizer synthesizer, AudioQuery audioQuery, int styleId) { + if (!Utils.isU32(styleId)) { + throw new IllegalArgumentException("styleId"); + } + this.synthesizer = synthesizer; + this.audioQuery = audioQuery; + this.styleId = styleId; + this.interrogativeUpspeak = false; + } + + /** + * 疑問文の調整を有効にするかどうか。 + * + * @param interrogativeUpspeak 疑問文の調整を有効にするかどうか。 + * @return {@link SynthesisConfigurator}。 + */ + @Nonnull + public SynthesisConfigurator interrogativeUpspeak(boolean interrogativeUpspeak) { + this.interrogativeUpspeak = interrogativeUpspeak; + return this; + } + + /** + * {@link AudioQuery} から音声合成する。 + * + * @return 音声データ。 + */ + @Nonnull + public byte[] execute() { + if (!Utils.isU32(styleId)) { + throw new IllegalArgumentException("styleId"); + } + Gson gson = new Gson(); + String queryJson = gson.toJson(this.audioQuery); + return synthesizer.rsSynthesis(queryJson, this.styleId, this.interrogativeUpspeak); + } + } + + /** {@link Synthesizer#tts} のオプション。 */ + public class TtsConfigurator { + private Synthesizer synthesizer; + private String text; + private int styleId; + private boolean kana; + private boolean interrogativeUpspeak; + + private TtsConfigurator(Synthesizer synthesizer, String text, int styleId) { + if (!Utils.isU32(styleId)) { + throw new IllegalArgumentException("styleId"); + } + this.synthesizer = synthesizer; + this.text = text; + this.styleId = styleId; + this.kana = false; + } + + /** + * 入力テキストをAquesTalk風記法として解釈するかどうか。 + * + * @param kana 入力テキストをAquesTalk風記法として解釈するかどうか。 + * @return {@link CreateAudioQueryConfigurator}。 + */ + @Nonnull + public TtsConfigurator kana(boolean kana) { + this.kana = kana; + return this; + } + + /** + * 疑問文の調整を有効にするかどうか。 + * + * @param interrogativeUpspeak 疑問文の調整を有効にするかどうか。 + * @return {@link SynthesisConfigurator}。 + */ + @Nonnull + public TtsConfigurator interrogativeUpspeak(boolean interrogativeUpspeak) { + this.interrogativeUpspeak = interrogativeUpspeak; + return this; + } + + /** + * {@link AudioQuery} から音声合成する。 + * + * @return 音声データ。 + */ + @Nonnull + public byte[] execute() { + if (!Utils.isU32(styleId)) { + throw new IllegalArgumentException("styleId"); + } + return synthesizer.rsTts(this.text, this.styleId, this.kana, this.interrogativeUpspeak); + } + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/UserDict.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/UserDict.java new file mode 100644 index 000000000..e31d56338 --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/UserDict.java @@ -0,0 +1,271 @@ +package jp.hiroshiba.voicevoxcore; + +import com.google.gson.Gson; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import com.google.gson.internal.LinkedTreeMap; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; + +import java.lang.ref.Cleaner; +import java.util.HashMap; +import javax.annotation.Nonnull; + +/** ユーザー辞書。 */ +public class UserDict extends Dll { + private long handle; + private static final Cleaner cleaner = Cleaner.create(); + + /** ユーザー辞書を作成する。 */ + public UserDict() { + rsNew(); + + cleaner.register(this, () -> rsDrop()); + } + + /** + * 単語を追加する。 + * + * @param word 追加する単語。 + * @return 追加した単語のUUID。 + */ + @Nonnull + public String addWord(Word word) { + Gson gson = new Gson(); + String wordJson = gson.toJson(word); + + return rsAddWord(wordJson); + } + + /** + * 単語を更新する。 + * + * @param uuid 更新する単語のUUID。 + * @param word 新しい単語のデータ。 + */ + public void updateWord(String uuid, Word word) { + Gson gson = new Gson(); + String wordJson = gson.toJson(word); + + rsUpdateWord(uuid, wordJson); + } + + /** + * 単語を削除する。 + * + * @param uuid 削除する単語のUUID。 + */ + public void removeWord(String uuid) { + rsRemoveWord(uuid); + } + + /** + * ユーザー辞書をインポートする。 + * + * @param dict インポートするユーザー辞書。 + */ + public void importDict(UserDict dict) { + rsImportDict(dict); + } + + /** + * ユーザー辞書を読み込む。 + * + * @param path ユーザー辞書のパス。 + */ + public void load(String path) { + rsLoad(path); + } + + /** + * ユーザー辞書を保存する。 + * + * @param path ユーザー辞書のパス。 + */ + public void save(String path) { + rsSave(path); + } + + /** + * ユーザー辞書の単語を取得する。 + * + * @return ユーザー辞書の単語。 + */ + @Nonnull + public HashMap toHashMap() { + String json = rsGetWords(); + Gson gson = new Gson(); + @SuppressWarnings("unchecked") + HashMap> rawWords = gson.fromJson(json, HashMap.class); + if (rawWords == null) { + throw new NullPointerException("words"); + } + HashMap words = new HashMap<>(); + rawWords.forEach( + (uuid, rawWord) -> { + Word word = gson.fromJson(gson.toJson(rawWord), Word.class); + if (word == null) { + throw new NullPointerException("word"); + } + words.put(uuid, word); + }); + + return words; + } + + private native void rsNew(); + + @Nonnull + private native String rsAddWord(String word); + + private native void rsUpdateWord(String uuid, String word); + + private native void rsRemoveWord(String uuid); + + private native void rsImportDict(UserDict dict); + + private native void rsLoad(String path); + + private native void rsSave(String path); + + @Nonnull + private native String rsGetWords(); + + private native void rsDrop(); + + @Nonnull + private static native String rsToZenkaku(String surface); + + private static native void rsValidatePronunciation(String pronunciation); + + /** ユーザー辞書の単語。 */ + public static class Word { + /** 単語の表層形。 */ + @SerializedName("surface") + @Expose + @Nonnull + public String surface; + + /** 単語の発音。 発音として有効なカタカナである必要がある。 */ + @SerializedName("pronunciation") + @Expose + @Nonnull + public String pronunciation; + + /** + * 単語の種類。 + * + * @see Type + */ + @SerializedName("word_type") + @Expose + @Nonnull + public Type wordType; + + /** アクセント型。 音が下がる場所を指す。 */ + @SerializedName("accent_type") + @Expose + public int accentType; + + /** 単語の優先度。 0から10までの整数。 数字が大きいほど優先度が高くなる。 1から9までの値を指定することを推奨。 */ + @SerializedName("priority") + @Expose + @Min(0) + @Max(10) + public int priority; + + /** + * UserDict.Wordを作成する。 + * + * @param surface 言葉の表層形。 + * @param pronunciation 言葉の発音。 + * @throws IllegalArgumentException pronunciationが不正な場合。 + */ + public Word(String surface, String pronunciation) { + if (surface == null) { + throw new NullPointerException("surface"); + } + if (pronunciation == null) { + throw new NullPointerException("pronunciation"); + } + + this.surface = rsToZenkaku(surface); + rsValidatePronunciation(pronunciation); + this.pronunciation = pronunciation; + this.wordType = Type.COMMON_NOUN; + this.accentType = 0; + this.priority = 5; + } + + /** + * 単語の種類を設定する。 + * + * @param wordType 単語の種類。 + * @return このインスタンス。 + */ + public Word wordType(Type wordType) { + if (wordType == null) { + throw new NullPointerException("wordType"); + } + this.wordType = wordType; + return this; + } + + /** + * アクセント型を設定する。 + * + * @param accentType アクセント型。 + * @return このインスタンス。 + */ + public Word accentType(int accentType) { + if (accentType < 0) { + throw new IllegalArgumentException("accentType"); + } + this.accentType = accentType; + return this; + } + + /** + * 優先度を設定する。 + * + * @param priority 優先度。 + * @return このインスタンス。 + * @throws IllegalArgumentException priorityが0未満または10より大きい場合。 + */ + public Word priority(int priority) { + if (priority < 0 || priority > 10) { + throw new IllegalArgumentException("priority"); + } + this.priority = priority; + return this; + } + + /** 単語の種類。 */ + public static enum Type { + /** 固有名詞。 */ + @SerializedName("PROPER_NOUN") + @Expose + PROPER_NOUN, + + /** 一般名詞。 */ + @SerializedName("COMMON_NOUN") + @Expose + COMMON_NOUN, + + /** 動詞。 */ + @SerializedName("VERB") + @Expose + VERB, + + /** 形容詞。 */ + @SerializedName("ADJECTIVE") + @Expose + ADJECTIVE, + + /** 語尾。 */ + @SerializedName("SUFFIX") + @Expose + SUFFIX, + } + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/Utils.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/Utils.java new file mode 100644 index 000000000..19b154cbc --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/Utils.java @@ -0,0 +1,15 @@ +package jp.hiroshiba.voicevoxcore; + +class Utils { + static boolean isU8(int value) { + return value >= 0 && value <= 255; + } + + static boolean isU16(int value) { + return value >= 0 && value <= 65535; + } + + static boolean isU32(long value) { + return value >= 0 && value <= 4294967295L; + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/VoiceModel.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/VoiceModel.java new file mode 100644 index 000000000..6b157fdba --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/VoiceModel.java @@ -0,0 +1,98 @@ +package jp.hiroshiba.voicevoxcore; + +import com.google.gson.Gson; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import java.lang.ref.Cleaner; +import javax.annotation.Nonnull; + +/** 音声モデル。 */ +public class VoiceModel extends Dll { + private long handle; + private static final Cleaner cleaner = Cleaner.create(); + + /** ID。 */ + @Nonnull public final String id; + + /** メタ情報。 */ + @Nonnull public final SpeakerMeta[] metas; + + public VoiceModel(String modelPath) { + rsFromPath(modelPath); + id = rsGetId(); + String metasJson = rsGetMetasJson(); + Gson gson = new Gson(); + SpeakerMeta[] rawMetas = gson.fromJson(metasJson, SpeakerMeta[].class); + if (rawMetas == null) { + throw new RuntimeException("Failed to parse metasJson"); + } + metas = rawMetas; + + cleaner.register(this, () -> rsDrop()); + } + + private native void rsFromPath(String modelPath); + + @Nonnull + private native String rsGetId(); + + @Nonnull + private native String rsGetMetasJson(); + + private native void rsDrop(); + + /** 話者(speaker)のメタ情報。 */ + public static class SpeakerMeta { + /** 話者名。 */ + @SerializedName("name") + @Expose + @Nonnull + final String name; + + /** 話者に属するスタイル。 */ + @SerializedName("styles") + @Expose + @Nonnull + final StyleMeta[] styles; + + /** 話者のUUID。 */ + @SerializedName("speaker_uuid") + @Expose + @Nonnull + final String speakerUuid; + + /** 話者のバージョン。 */ + @SerializedName("version") + @Expose + @Nonnull + final String version; + + private SpeakerMeta() { + // GSONからコンストラクトするため、このメソッドは呼ばれることは無い。 + // このメソッドは@Nonnullを満たすために必要。 + this.name = ""; + this.styles = new StyleMeta[0]; + this.speakerUuid = ""; + this.version = ""; + } + } + + /** スタイル(style)のメタ情報。 */ + public static class StyleMeta { + /** スタイル名。 */ + @SerializedName("name") + @Expose + @Nonnull + final String name; + + /** スタイルID。 */ + @SerializedName("id") + @Expose + final int id; + + private StyleMeta() { + this.name = ""; + this.id = 0; + } + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/VoicevoxException.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/VoicevoxException.java new file mode 100644 index 000000000..3bf6f53a4 --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/VoicevoxException.java @@ -0,0 +1,8 @@ +package jp.hiroshiba.voicevoxcore; + +/** VOICEVOX COREのエラー。 */ +public class VoicevoxException extends RuntimeException { + public VoicevoxException(String message) { + super(message); + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/resources/dll/README.md b/crates/voicevox_core_java_api/lib/src/main/resources/dll/README.md new file mode 100644 index 000000000..1975c39b6 --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/resources/dll/README.md @@ -0,0 +1,9 @@ +このディレクトリに JNI 用に読み込まれる DLL を配置します。 +ディレクトリ名は以下のうちのいずれかになります。 + +- `windows-x64` +- `windows-x86` +- `linux-x64` +- `linux-arm64` +- `macos-x64` +- `macos-arm64` diff --git a/crates/voicevox_core_java_api/lib/src/main/resources/jniLibs/arm64-x8a/.gitkeep b/crates/voicevox_core_java_api/lib/src/main/resources/jniLibs/arm64-x8a/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/crates/voicevox_core_java_api/lib/src/main/resources/jniLibs/x86_64/.gitkeep b/crates/voicevox_core_java_api/lib/src/main/resources/jniLibs/x86_64/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/MetaTest.java b/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/MetaTest.java new file mode 100644 index 000000000..741f84e79 --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/MetaTest.java @@ -0,0 +1,20 @@ +/* + * This Java source file was generated by the Gradle 'init' task. + */ +package jp.hiroshiba.voicevoxcore; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.File; +import org.junit.jupiter.api.Test; + +class MetaTest { + @Test + void checkLoad() { + // cwdはvoicevox_core/crates/voicevox_core_java_api/lib + String cwd = System.getProperty("user.dir"); + File path = new File(cwd + "/../../../model/sample.vvm"); + VoiceModel model = new VoiceModel(path.getAbsolutePath()); + assertNotNull(model.metas); + } +} diff --git a/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/SynthesizerTest.java b/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/SynthesizerTest.java new file mode 100644 index 000000000..fe9b8220b --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/SynthesizerTest.java @@ -0,0 +1,96 @@ +/* + * 音声合成のテスト。 + * ttsaudioQuery -> synthesisの順に実行する。 + */ +package jp.hiroshiba.voicevoxcore; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class SynthesizerTest extends TestUtils { + @FunctionalInterface + interface MoraCheckCallback { + boolean check(Mora mora, Mora otherMora); + } + + boolean checkAllMoras( + List accentPhrases, + List otherAccentPhrases, + MoraCheckCallback checker) { + for (int i = 0; i < accentPhrases.size(); i++) { + AccentPhrase accentPhrase = accentPhrases.get(i); + for (int j = 0; j < accentPhrase.moras.size(); j++) { + Mora mora = accentPhrase.moras.get(j); + Mora otherMora = otherAccentPhrases.get(i).moras.get(j); + if (!checker.check(mora, otherMora)) { + return false; + } + } + } + return true; + } + + @Test + void checkModel() { + VoiceModel model = loadModel(); + OpenJtalk openJtalk = loadOpenJtalk(); + Synthesizer synthesizer = Synthesizer.builder(openJtalk).build(); + synthesizer.loadVoiceModel(model); + assertTrue(synthesizer.isLoadedVoiceModel(model.id)); + synthesizer.unloadVoiceModel(model.id); + assertFalse(synthesizer.isLoadedVoiceModel(model.id)); + } + + @Test + void checkAudioQuery() { + VoiceModel model = loadModel(); + OpenJtalk openJtalk = loadOpenJtalk(); + Synthesizer synthesizer = Synthesizer.builder(openJtalk).build(); + synthesizer.loadVoiceModel(model); + AudioQuery query = synthesizer.createAudioQuery("こんにちは", model.metas[0].styles[0].id).execute(); + + synthesizer.synthesis(query, model.metas[0].styles[0].id).execute(); + } + + @Test + void checkAccentPhrases() { + VoiceModel model = loadModel(); + OpenJtalk openJtalk = loadOpenJtalk(); + Synthesizer synthesizer = Synthesizer.builder(openJtalk).build(); + synthesizer.loadVoiceModel(model); + List accentPhrases = + synthesizer.createAccentPhrases("こんにちは", model.metas[0].styles[0].id).execute(); + List accentPhrases2 = + synthesizer.replaceMoraPitch(accentPhrases, model.metas[1].styles[0].id); + assertTrue( + checkAllMoras( + accentPhrases, accentPhrases2, (mora, otherMora) -> mora.pitch != otherMora.pitch)); + List accentPhrases3 = + synthesizer.replacePhonemeLength(accentPhrases, model.metas[1].styles[0].id); + assertTrue( + checkAllMoras( + accentPhrases, + accentPhrases3, + (mora, otherMora) -> mora.vowelLength != otherMora.vowelLength)); + List accentPhrases4 = + synthesizer.replaceMoraData(accentPhrases, model.metas[1].styles[0].id); + assertTrue( + checkAllMoras( + accentPhrases, + accentPhrases4, + (mora, otherMora) -> + mora.pitch != otherMora.pitch && mora.vowelLength != otherMora.vowelLength)); + } + + @Test + void checkTts() { + VoiceModel model = loadModel(); + OpenJtalk openJtalk = loadOpenJtalk(); + Synthesizer synthesizer = Synthesizer.builder(openJtalk).build(); + synthesizer.loadVoiceModel(model); + synthesizer.tts("こんにちは", model.metas[0].styles[0].id).execute(); + } +} diff --git a/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/TestUtils.java b/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/TestUtils.java new file mode 100644 index 000000000..670eddbdb --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/TestUtils.java @@ -0,0 +1,28 @@ +package jp.hiroshiba.voicevoxcore; + +import java.io.File; + +class TestUtils { + VoiceModel loadModel() { + // cwdはvoicevox_core/crates/voicevox_core_java_api/lib + String cwd = System.getProperty("user.dir"); + File path = new File(cwd + "/../../../model/sample.vvm"); + + try { + return new VoiceModel(path.getCanonicalPath()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + OpenJtalk loadOpenJtalk() { + String cwd = System.getProperty("user.dir"); + File path = new File(cwd + "/../../test_util/data/open_jtalk_dic_utf_8-1.11"); + + try { + return new OpenJtalk(path.getCanonicalPath()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/UserDictTest.java b/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/UserDictTest.java new file mode 100644 index 000000000..a7847efba --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/test/java/jp/hiroshiba/voicevoxcore/UserDictTest.java @@ -0,0 +1,70 @@ +package jp.hiroshiba.voicevoxcore; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; + +class UserDictTest extends TestUtils { + + // 辞書ロードのテスト。 + // 辞書ロード前後でkanaが異なることを確認する + @Test + void checkLoad() { + VoiceModel model = loadModel(); + OpenJtalk openJtalk = loadOpenJtalk(); + Synthesizer synthesizer = Synthesizer.builder(openJtalk).build(); + UserDict userDict = new UserDict(); + synthesizer.loadVoiceModel(model); + AudioQuery query1 = + synthesizer + .createAudioQuery( + "this_word_should_not_exist_in_default_dictionary", model.metas[0].styles[0].id) + .execute(); + + userDict.addWord(new UserDict.Word("this_word_should_not_exist_in_default_dictionary", "テスト")); + openJtalk.useUserDict(userDict); + AudioQuery query2 = + synthesizer + .createAudioQuery( + "this_word_should_not_exist_in_default_dictionary", model.metas[0].styles[0].id) + .execute(); + assertTrue(query1.kana != query2.kana); + } + + // 辞書操作のテスト。 + @Test + void checkManipulation() throws Exception { + UserDict userDict = new UserDict(); + // 単語追加 + String uuid = userDict.addWord(new UserDict.Word("hoge", "ホゲ")); + assertTrue(userDict.toHashMap().get(uuid) != null); + + // 単語更新 + userDict.updateWord(uuid, new UserDict.Word("hoge", "ホゲホゲ")); + assertTrue(userDict.toHashMap().get(uuid).pronunciation.equals("ホゲホゲ")); + + // 単語削除 + userDict.removeWord(uuid); + assertTrue(userDict.toHashMap().get(uuid) == null); + + // 辞書のインポート + userDict.addWord(new UserDict.Word("hoge", "ホゲ")); + UserDict userDict2 = new UserDict(); + userDict2.addWord(new UserDict.Word("fuga", "フガ")); + userDict.importDict(userDict2); + assertTrue(userDict.toHashMap().size() == 2); + + // 辞書の保存/読み込み + Path path = Files.createTempFile("voicevox_user_dict", ".json"); + try { + UserDict userDict3 = new UserDict(); + userDict.save(path.toString()); + userDict3.load(path.toString()); + assertTrue(userDict3.toHashMap().size() == 2); + } finally { + Files.deleteIfExists(path); + } + } +} diff --git a/crates/voicevox_core_java_api/settings.gradle b/crates/voicevox_core_java_api/settings.gradle new file mode 100644 index 000000000..7d07347eb --- /dev/null +++ b/crates/voicevox_core_java_api/settings.gradle @@ -0,0 +1,7 @@ +plugins { + // Apply the foojay-resolver plugin to allow automatic download of JDKs + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.4.0' +} + +rootProject.name = 'jp.hiroshiba.voicevoxcore' +include('lib') diff --git a/crates/voicevox_core_java_api/src/common.rs b/crates/voicevox_core_java_api/src/common.rs new file mode 100644 index 000000000..d7aeb720b --- /dev/null +++ b/crates/voicevox_core_java_api/src/common.rs @@ -0,0 +1,107 @@ +use anyhow::Result; +use jni::JNIEnv; +use once_cell::sync::Lazy; +use tokio::runtime::Runtime; + +pub static RUNTIME: Lazy = Lazy::new(|| { + if cfg!(target_os = "android") { + android_logger::init_once( + android_logger::Config::default() + .with_tag("VoicevoxCore") + .with_filter( + android_logger::FilterBuilder::new() + .parse("error,voicevox_core=info,voicevox_core_java_api=info,onnxruntime=error") + .build(), + ), + ); + } else { + // TODO: Android以外でのログ出力を良い感じにする。(System.Loggerを使う?) + use chrono::SecondsFormat; + use std::{ + env, fmt, + io::{self, IsTerminal, Write}, + }; + use tracing_subscriber::{fmt::format::Writer, EnvFilter}; + + let _ = tracing_subscriber::fmt() + .with_env_filter(if env::var_os(EnvFilter::DEFAULT_ENV).is_some() { + EnvFilter::from_default_env() + } else { + "error,voicevox_core=info,voicevox_core_c_api=info,onnxruntime=error".into() + }) + .with_timer(local_time as fn(&mut Writer<'_>) -> _) + .with_ansi(out().is_terminal() && env_allows_ansi()) + .with_writer(out) + .try_init(); + + fn local_time(wtr: &mut Writer<'_>) -> fmt::Result { + // ローカル時刻で表示はするが、そのフォーマットはtracing-subscriber本来のものに近いようにする。 + // https://github.com/tokio-rs/tracing/blob/tracing-subscriber-0.3.16/tracing-subscriber/src/fmt/time/datetime.rs#L235-L241 + wtr.write_str(&chrono::Local::now().to_rfc3339_opts(SecondsFormat::Micros, false)) + } + + fn out() -> impl IsTerminal + Write { + io::stderr() + } + + fn env_allows_ansi() -> bool { + // https://docs.rs/termcolor/1.2.0/src/termcolor/lib.rs.html#245-291 + // ただしWindowsではPowerShellっぽかったらそのまま許可する。 + // ちゃんとやるなら`ENABLE_VIRTUAL_TERMINAL_PROCESSING`をチェックするなり、そもそも + // fwdansiとかでWin32の色に変換するべきだが、面倒。 + env::var_os("TERM").map_or( + cfg!(windows) && env::var_os("PSModulePath").is_some(), + |term| term != "dumb", + ) && env::var_os("NO_COLOR").is_none() + } + } + Runtime::new().unwrap() +}); + +#[macro_export] +macro_rules! object { + ($name: literal) => { + concat!("jp/hiroshiba/voicevoxcore/", $name) + }; +} +#[macro_export] +macro_rules! object_type { + ($name: literal) => { + concat!("Ljp/hiroshiba/voicevoxcore/", $name, ";") + }; +} +#[macro_export] +macro_rules! enum_object { + ($env: ident, $name: literal, $variant: literal) => { + $env.get_static_field(object!($name), $variant, object_type!($name)) + .unwrap_or_else(|_| { + panic!( + "Failed to get field {}", + concat!($variant, "L", object!($name), ";") + ) + }) + .l() + }; +} + +pub fn throw_if_err(mut env: JNIEnv, fallback: T, inner: F) -> T +where + F: FnOnce(&mut JNIEnv) -> Result, +{ + match inner(&mut env) { + Ok(value) => value as _, + Err(error) => { + // Java側の例外は無視する。 + // env.exception_clear()してもいいが、errorのメッセージは"Java exception was thrown" + // となり、デバッグが困難になるため、そのままにしておく。 + if !env.exception_check().unwrap_or(false) { + env.throw_new( + "jp/hiroshiba/voicevoxcore/VoicevoxException", + error.to_string(), + ) + .unwrap_or_else(|_| panic!("Failed to throw exception, original error: {}", error)); + } + fallback + } + } +} diff --git a/crates/voicevox_core_java_api/src/lib.rs b/crates/voicevox_core_java_api/src/lib.rs new file mode 100644 index 000000000..4ea35c0b2 --- /dev/null +++ b/crates/voicevox_core_java_api/src/lib.rs @@ -0,0 +1,5 @@ +mod common; +mod open_jtalk; +mod synthesizer; +mod user_dict; +mod voice_model; diff --git a/crates/voicevox_core_java_api/src/open_jtalk.rs b/crates/voicevox_core_java_api/src/open_jtalk.rs new file mode 100644 index 000000000..98feaacb0 --- /dev/null +++ b/crates/voicevox_core_java_api/src/open_jtalk.rs @@ -0,0 +1,71 @@ +use std::sync::{Arc, Mutex}; + +use crate::common::throw_if_err; +use jni::{ + objects::{JObject, JString}, + JNIEnv, +}; +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_OpenJtalk_rsNewWithoutDic<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, +) { + throw_if_err(env, (), |env| { + let internal = voicevox_core::OpenJtalk::new_without_dic(); + + env.set_rust_field(&this, "handle", Arc::new(internal))?; + Ok(()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_OpenJtalk_rsNewWithInitialize<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, + open_jtalk_dict_dir: JString<'local>, +) { + throw_if_err(env, (), |env| { + let open_jtalk_dict_dir = env.get_string(&open_jtalk_dict_dir)?; + let open_jtalk_dict_dir = open_jtalk_dict_dir.to_str()?; + + let internal = voicevox_core::OpenJtalk::new_with_initialize(open_jtalk_dict_dir)?; + env.set_rust_field(&this, "handle", Arc::new(internal))?; + + Ok(()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_OpenJtalk_rsUseUserDict<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, + user_dict: JObject<'local>, +) { + throw_if_err(env, (), |env| { + let internal = env + .get_rust_field::<_, _, Arc>(&this, "handle")? + .clone(); + + let user_dict = env + .get_rust_field::<_, _, Arc>>(&user_dict, "handle")? + .clone(); + + { + let user_dict = user_dict.lock().unwrap(); + internal.use_user_dict(&user_dict)? + } + + Ok(()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_OpenJtalk_rsDrop<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, +) { + throw_if_err(env, (), |env| { + env.take_rust_field(&this, "handle")?; + Ok(()) + }) +} diff --git a/crates/voicevox_core_java_api/src/synthesizer.rs b/crates/voicevox_core_java_api/src/synthesizer.rs new file mode 100644 index 000000000..de038c70c --- /dev/null +++ b/crates/voicevox_core_java_api/src/synthesizer.rs @@ -0,0 +1,382 @@ +use crate::{ + common::{throw_if_err, RUNTIME}, + enum_object, object, object_type, +}; + +use anyhow::anyhow; +use jni::{ + objects::{JObject, JString}, + sys::{jboolean, jint, jobject}, + JNIEnv, +}; +use std::sync::{Arc, Mutex}; + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsNewWithInitialize<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, + open_jtalk: JObject<'local>, + builder: JObject<'local>, +) { + throw_if_err(env, (), |env| { + let mut options = voicevox_core::InitializeOptions::default(); + + let acceleration_mode = env + .get_field( + &builder, + "accelerationMode", + object_type!("Synthesizer$AccelerationMode"), + )? + .l()?; + + if !acceleration_mode.is_null() { + let auto = enum_object!(env, "Synthesizer$AccelerationMode", "AUTO")?; + let cpu = enum_object!(env, "Synthesizer$AccelerationMode", "CPU")?; + let gpu = enum_object!(env, "Synthesizer$AccelerationMode", "GPU")?; + options.acceleration_mode = if env.is_same_object(&acceleration_mode, auto)? { + voicevox_core::AccelerationMode::Auto + } else if env.is_same_object(&acceleration_mode, cpu)? { + voicevox_core::AccelerationMode::Cpu + } else if env.is_same_object(&acceleration_mode, gpu)? { + voicevox_core::AccelerationMode::Gpu + } else { + return Err(anyhow!("invalid acceleration mode".to_string(),)); + }; + } + let cpu_num_threads = env.get_field(&builder, "cpuNumThreads", "I")?; + options.cpu_num_threads = cpu_num_threads.i().expect("cpuNumThreads is not integer") as u16; + + let load_all_models = env.get_field(&builder, "loadAllModels", "Z")?; + options.load_all_models = load_all_models.z().expect("loadAllModels is not boolean"); + + let open_jtalk = env + .get_rust_field::<_, _, Arc>(&open_jtalk, "handle")? + .clone(); + let internal = RUNTIME.block_on(voicevox_core::Synthesizer::new_with_initialize( + open_jtalk, + Box::leak(Box::new(options)), + ))?; + env.set_rust_field(&this, "handle", Arc::new(Mutex::new(internal)))?; + Ok(()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsLoadVoiceModel<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, + model: JObject<'local>, +) { + throw_if_err(env, (), |env| { + let model = env + .get_rust_field::<_, _, Arc>(&model, "handle")? + .clone(); + let internal = env + .get_rust_field::<_, _, Arc>>(&this, "handle")? + .clone(); + { + let internal = internal.lock().unwrap(); + RUNTIME.block_on(internal.load_voice_model(&model))?; + } + Ok(()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsUnloadVoiceModel<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, + model_id: JString<'local>, +) { + throw_if_err(env, (), |env| { + let model_id: String = env.get_string(&model_id)?.into(); + + let internal = env + .get_rust_field::<_, _, Arc>>(&this, "handle")? + .clone(); + + { + let internal = internal.lock().unwrap(); + + internal.unload_voice_model(&voicevox_core::VoiceModelId::new(model_id))?; + } + + Ok(()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsIsLoadedVoiceModel< + 'local, +>( + env: JNIEnv<'local>, + this: JObject<'local>, + model_id: JString<'local>, +) -> jboolean { + throw_if_err(env, false, |env| { + let model_id: String = env.get_string(&model_id)?.into(); + + let internal = env + .get_rust_field::<_, _, Arc>>(&this, "handle")? + .clone(); + + let is_loaded = { + let internal = internal.lock().unwrap(); + internal.is_loaded_voice_model(&voicevox_core::VoiceModelId::new(model_id)) + }; + + Ok(is_loaded) + }) + .into() +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsAudioQuery<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, + text: JString<'local>, + style_id: jint, + kana: jboolean, +) -> jobject { + throw_if_err(env, std::ptr::null_mut(), |env| { + let text: String = env.get_string(&text)?.into(); + let style_id = style_id as u32; + + let internal = env + .get_rust_field::<_, _, Arc>>(&this, "handle")? + .clone(); + + let audio_query = { + let internal = internal.lock().unwrap(); + let options = voicevox_core::AudioQueryOptions { + kana: kana != 0, + // ..Default::default() + }; + RUNTIME.block_on(internal.audio_query( + &text, + voicevox_core::StyleId::new(style_id), + &options, + ))? + }; + + let query_json = serde_json::to_string(&audio_query)?; + + let j_audio_query = env.new_string(query_json)?; + + Ok(j_audio_query.into_raw()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsAccentPhrases<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, + text: JString<'local>, + style_id: jint, + kana: jboolean, +) -> jobject { + throw_if_err(env, std::ptr::null_mut(), |env| { + let text: String = env.get_string(&text)?.into(); + let style_id = style_id as u32; + + let internal = env + .get_rust_field::<_, _, Arc>>(&this, "handle")? + .clone(); + + let accent_phrases = { + let internal = internal.lock().unwrap(); + let options = voicevox_core::AccentPhrasesOptions { + kana: kana != 0, + // ..Default::default() + }; + RUNTIME.block_on(internal.create_accent_phrases( + &text, + voicevox_core::StyleId::new(style_id), + &options, + ))? + }; + + let query_json = serde_json::to_string(&accent_phrases)?; + + let j_accent_phrases = env.new_string(query_json)?; + + Ok(j_accent_phrases.into_raw()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsReplaceMoraData<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, + accent_phrases_json: JString<'local>, + style_id: jint, +) -> jobject { + throw_if_err(env, std::ptr::null_mut(), |env| { + let accent_phrases_json: String = env.get_string(&accent_phrases_json)?.into(); + let accent_phrases: Vec = + serde_json::from_str(&accent_phrases_json)?; + let style_id = style_id as u32; + + let internal = env + .get_rust_field::<_, _, Arc>>(&this, "handle")? + .clone(); + + let replaced_accent_phrases = { + let internal = internal.lock().unwrap(); + RUNTIME.block_on( + internal.replace_mora_data(&accent_phrases, voicevox_core::StyleId::new(style_id)), + )? + }; + + let replaced_accent_phrases_json = serde_json::to_string(&replaced_accent_phrases)?; + + Ok(env.new_string(replaced_accent_phrases_json)?.into_raw()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsReplacePhonemeLength< + 'local, +>( + env: JNIEnv<'local>, + this: JObject<'local>, + accent_phrases_json: JString<'local>, + style_id: jint, +) -> jobject { + throw_if_err(env, std::ptr::null_mut(), |env| { + let accent_phrases_json: String = env.get_string(&accent_phrases_json)?.into(); + let accent_phrases: Vec = + serde_json::from_str(&accent_phrases_json)?; + let style_id = style_id as u32; + + let internal = env + .get_rust_field::<_, _, Arc>>(&this, "handle")? + .clone(); + + let replaced_accent_phrases = { + let internal = internal.lock().unwrap(); + RUNTIME.block_on( + internal + .replace_phoneme_length(&accent_phrases, voicevox_core::StyleId::new(style_id)), + )? + }; + + let replaced_accent_phrases_json = serde_json::to_string(&replaced_accent_phrases)?; + + Ok(env.new_string(replaced_accent_phrases_json)?.into_raw()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsReplaceMoraPitch<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, + accent_phrases_json: JString<'local>, + style_id: jint, +) -> jobject { + throw_if_err(env, std::ptr::null_mut(), |env| { + let accent_phrases_json: String = env.get_string(&accent_phrases_json)?.into(); + let accent_phrases: Vec = + serde_json::from_str(&accent_phrases_json)?; + let style_id = style_id as u32; + + let internal = env + .get_rust_field::<_, _, Arc>>(&this, "handle")? + .clone(); + + let replaced_accent_phrases = { + let internal = internal.lock().unwrap(); + RUNTIME.block_on( + internal.replace_mora_pitch(&accent_phrases, voicevox_core::StyleId::new(style_id)), + )? + }; + + let replaced_accent_phrases_json = serde_json::to_string(&replaced_accent_phrases)?; + + Ok(env.new_string(replaced_accent_phrases_json)?.into_raw()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsSynthesis<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, + query_json: JString<'local>, + style_id: jint, + enable_interrogative_upspeak: jboolean, +) -> jobject { + throw_if_err(env, std::ptr::null_mut(), |env| { + let audio_query: String = env.get_string(&query_json)?.into(); + let audio_query: voicevox_core::AudioQueryModel = serde_json::from_str(&audio_query)?; + let style_id = style_id as u32; + + let internal = env + .get_rust_field::<_, _, Arc>>(&this, "handle")? + .clone(); + + let wave = { + let internal = internal.lock().unwrap(); + let options = voicevox_core::SynthesisOptions { + enable_interrogative_upspeak: enable_interrogative_upspeak != 0, + // ..Default::default() + }; + RUNTIME.block_on(internal.synthesis( + &audio_query, + voicevox_core::StyleId::new(style_id), + &options, + ))? + }; + + let j_bytes = env.byte_array_from_slice(&wave)?; + + Ok(j_bytes.into_raw()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsTts<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, + query_json: JString<'local>, + style_id: jint, + kana: jboolean, + enable_interrogative_upspeak: jboolean, +) -> jobject { + throw_if_err(env, std::ptr::null_mut(), |env| { + let text: String = env.get_string(&query_json)?.into(); + let style_id = style_id as u32; + + let internal = env + .get_rust_field::<_, _, Arc>>(&this, "handle")? + .clone(); + + let wave = { + let internal = internal.lock().unwrap(); + let options = voicevox_core::TtsOptions { + kana: kana != 0, + enable_interrogative_upspeak: enable_interrogative_upspeak != 0, + // ..Default::default() + }; + RUNTIME.block_on(internal.tts( + &text, + voicevox_core::StyleId::new(style_id), + &options, + ))? + }; + + let j_bytes = env.byte_array_from_slice(&wave)?; + + Ok(j_bytes.into_raw()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsDrop<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, +) { + throw_if_err(env, (), |env| { + env.take_rust_field(&this, "handle")?; + Ok(()) + }) +} diff --git a/crates/voicevox_core_java_api/src/user_dict.rs b/crates/voicevox_core_java_api/src/user_dict.rs new file mode 100644 index 000000000..a89c11d8e --- /dev/null +++ b/crates/voicevox_core_java_api/src/user_dict.rs @@ -0,0 +1,237 @@ +use jni::objects::JClass; +use std::sync::{Arc, Mutex}; + +use crate::common::throw_if_err; +use jni::{ + objects::{JObject, JString}, + sys::jobject, + JNIEnv, +}; + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_UserDict_rsNew<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, +) { + throw_if_err(env, (), |env| { + let internal = voicevox_core::UserDict::new(); + + env.set_rust_field(&this, "handle", Arc::new(Mutex::new(internal)))?; + + Ok(()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_UserDict_rsAddWord<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, + word_json: JString<'local>, +) -> jobject { + throw_if_err(env, std::ptr::null_mut(), |env| { + let internal = env + .get_rust_field::<_, _, Arc>>(&this, "handle")? + .clone(); + + let word_json = env.get_string(&word_json)?; + let word_json = word_json.to_str()?; + + let word: voicevox_core::UserDictWord = serde_json::from_str(word_json)?; + + let uuid = { + let mut internal = internal.lock().unwrap(); + internal.add_word(word)? + }; + + let uuid = uuid.hyphenated().to_string(); + let uuid = env.new_string(uuid)?; + + Ok(uuid.into_raw()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_UserDict_rsUpdateWord<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, + uuid: JString<'local>, + word_json: JString<'local>, +) { + throw_if_err(env, (), |env| { + let internal = env + .get_rust_field::<_, _, Arc>>(&this, "handle")? + .clone(); + + let uuid = env.get_string(&uuid)?; + let uuid = uuid.to_str()?.parse()?; + let word_json = env.get_string(&word_json)?; + let word_json = word_json.to_str()?; + + let word: voicevox_core::UserDictWord = serde_json::from_str(word_json)?; + + { + let mut internal = internal.lock().unwrap(); + internal.update_word(uuid, word)?; + }; + + Ok(()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_UserDict_rsRemoveWord<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, + uuid: JString<'local>, +) { + throw_if_err(env, (), |env| { + let internal = env + .get_rust_field::<_, _, Arc>>(&this, "handle")? + .clone(); + + let uuid = env.get_string(&uuid)?; + let uuid = uuid.to_str()?.parse()?; + + { + let mut internal = internal.lock().unwrap(); + internal.remove_word(uuid)?; + }; + + Ok(()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_UserDict_rsImportDict<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, + other_dict: JObject<'local>, +) { + throw_if_err(env, (), |env| { + let internal = env + .get_rust_field::<_, _, Arc>>(&this, "handle")? + .clone(); + let other_dict = env + .get_rust_field::<_, _, Arc>>(&other_dict, "handle")? + .clone(); + + { + let mut internal = internal.lock().unwrap(); + let other_dict = other_dict.lock().unwrap(); + internal.import(&other_dict)?; + } + + Ok(()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_UserDict_rsLoad<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, + path: JString<'local>, +) { + throw_if_err(env, (), |env| { + let internal = env + .get_rust_field::<_, _, Arc>>(&this, "handle")? + .clone(); + + let path = env.get_string(&path)?; + let path = path.to_str()?; + + { + let mut internal = internal.lock().unwrap(); + internal.load(path)?; + }; + + Ok(()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_UserDict_rsSave<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, + path: JString<'local>, +) { + throw_if_err(env, (), |env| { + let internal = env + .get_rust_field::<_, _, Arc>>(&this, "handle")? + .clone(); + + let path = env.get_string(&path)?; + let path = path.to_str()?; + + { + let internal = internal.lock().unwrap(); + internal.save(path)?; + }; + + Ok(()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_UserDict_rsGetWords<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, +) -> jobject { + throw_if_err(env, std::ptr::null_mut(), |env| { + let internal = env + .get_rust_field::<_, _, Arc>>(&this, "handle")? + .clone(); + + let words = { + let internal = internal.lock().unwrap(); + serde_json::to_string(internal.words())? + }; + + let words = env.new_string(words)?; + + Ok(words.into_raw()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_UserDict_rsDrop<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, +) { + throw_if_err(env, (), |env| { + env.take_rust_field(&this, "handle")?; + Ok(()) + }) +} + +#[no_mangle] +extern "system" fn Java_jp_hiroshiba_voicevoxcore_UserDict_rsToZenkaku<'local>( + env: JNIEnv<'local>, + _cls: JClass<'local>, + text: JString<'local>, +) -> jobject { + throw_if_err(env, std::ptr::null_mut(), |env| { + let text = env.get_string(&text)?; + let text = text.to_str()?; + + let text = voicevox_core::to_zenkaku(text); + + let text = env.new_string(text)?; + Ok(text.into_raw()) + }) +} + +#[no_mangle] +extern "system" fn Java_jp_hiroshiba_voicevoxcore_UserDict_rsValidatePronunciation<'local>( + env: JNIEnv<'local>, + _cls: JClass<'local>, + text: JString<'local>, +) { + throw_if_err(env, (), |env| { + let text = env.get_string(&text)?; + let text = text.to_str()?; + + voicevox_core::validate_pronunciation(text)?; + + Ok(()) + }) +} diff --git a/crates/voicevox_core_java_api/src/voice_model.rs b/crates/voicevox_core_java_api/src/voice_model.rs new file mode 100644 index 000000000..33ae01288 --- /dev/null +++ b/crates/voicevox_core_java_api/src/voice_model.rs @@ -0,0 +1,71 @@ +use std::sync::Arc; + +use crate::common::{throw_if_err, RUNTIME}; +use jni::{ + objects::{JObject, JString}, + sys::jobject, + JNIEnv, +}; + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_VoiceModel_rsFromPath<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, + model_path: JString<'local>, +) { + throw_if_err(env, (), |env| { + let model_path = env.get_string(&model_path)?; + let model_path = model_path.to_str()?; + + let internal = RUNTIME.block_on(voicevox_core::VoiceModel::from_path(model_path))?; + + env.set_rust_field(&this, "handle", Arc::new(internal))?; + + Ok(()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_VoiceModel_rsGetId<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, +) -> jobject { + throw_if_err(env, std::ptr::null_mut(), |env| { + let internal = env + .get_rust_field::<_, _, Arc>(&this, "handle")? + .clone(); + + let id = internal.id().raw_voice_model_id(); + + let id = env.new_string(id)?; + + Ok(id.into_raw()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_VoiceModel_rsGetMetasJson<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, +) -> jobject { + throw_if_err(env, std::ptr::null_mut(), |env| { + let internal = env + .get_rust_field::<_, _, Arc>(&this, "handle")? + .clone(); + + let metas = internal.metas(); + let metas_json = serde_json::to_string(&metas)?; + Ok(env.new_string(metas_json)?.into_raw()) + }) +} + +#[no_mangle] +unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_VoiceModel_rsDrop<'local>( + env: JNIEnv<'local>, + this: JObject<'local>, +) { + throw_if_err(env, (), |env| { + env.take_rust_field(&this, "handle")?; + Ok(()) + }) +} diff --git a/docs/apis/index.html b/docs/apis/index.html index 9b580db6c..639602f9f 100644 --- a/docs/apis/index.html +++ b/docs/apis/index.html @@ -8,6 +8,7 @@