diff --git a/Cargo.lock b/Cargo.lock index e96ad5dbb..ee536102a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4355,8 +4355,8 @@ name = "voicevox_core_java_api" version = "0.0.0" dependencies = [ "android_logger", - "anyhow", "chrono", + "derive_more", "jni", "once_cell", "serde_json", @@ -4364,6 +4364,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "uuid", "voicevox_core", ] diff --git a/Cargo.toml b/Cargo.toml index 73e6f5cd4..b6237098a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ resolver = "2" anyhow = "1.0.65" async_zip = { version = "0.0.11", features = ["full"] } clap = { version = "4.0.10", features = ["derive"] } +derive_more = "0.99.17" easy-ext = "1.0.1" fs-err = { version = "2.9.0", features = ["tokio"] } futures = "0.3.26" diff --git a/crates/voicevox_core/Cargo.toml b/crates/voicevox_core/Cargo.toml index 988d694cf..c2159782c 100644 --- a/crates/voicevox_core/Cargo.toml +++ b/crates/voicevox_core/Cargo.toml @@ -14,7 +14,7 @@ async_zip.workspace = true cfg-if = "1.0.0" derive-getters.workspace = true derive-new = "0.5.9" -derive_more = "0.99.17" +derive_more.workspace = true duplicate = "1.0.0" easy-ext.workspace = true fs-err.workspace = true diff --git a/crates/voicevox_core_java_api/Cargo.toml b/crates/voicevox_core_java_api/Cargo.toml index bd546a243..ecf6d8f79 100644 --- a/crates/voicevox_core_java_api/Cargo.toml +++ b/crates/voicevox_core_java_api/Cargo.toml @@ -12,14 +12,15 @@ directml = ["voicevox_core/directml"] [dependencies] android_logger = "0.13.1" -anyhow.workspace = true chrono = "0.4.26" +derive_more.workspace = true jni = "0.21.1" once_cell.workspace = true serde_json.workspace = true tokio.workspace = true tracing.workspace = true tracing-subscriber.workspace = true +uuid.workspace = true voicevox_core.workspace = true [dev-dependencies] 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 index 5f3df9ea8..7204dbdd6 100644 --- 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 @@ -5,6 +5,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import jp.hiroshiba.voicevoxcore.exceptions.InferenceFailedException; +import jp.hiroshiba.voicevoxcore.exceptions.InvalidModelDataException; /** * 音声シンセサイザ。 @@ -28,7 +30,7 @@ protected void finalize() throws Throwable { * * @param voiceModel 読み込むモデル。 */ - public void loadVoiceModel(VoiceModel voiceModel) { + public void loadVoiceModel(VoiceModel voiceModel) throws InvalidModelDataException { rsLoadVoiceModel(voiceModel); } @@ -59,7 +61,8 @@ public boolean isLoadedVoiceModel(String voiceModelId) { * @return {@link AudioQuery}。 */ @Nonnull - public AudioQuery createAudioQueryFromKana(String kana, int styleId) { + public AudioQuery createAudioQueryFromKana(String kana, int styleId) + throws InferenceFailedException { if (!Utils.isU32(styleId)) { throw new IllegalArgumentException("styleId"); } @@ -81,7 +84,7 @@ public AudioQuery createAudioQueryFromKana(String kana, int styleId) { * @return {@link AudioQuery}。 */ @Nonnull - public AudioQuery createAudioQuery(String text, int styleId) { + public AudioQuery createAudioQuery(String text, int styleId) throws InferenceFailedException { if (!Utils.isU32(styleId)) { throw new IllegalArgumentException("styleId"); } @@ -103,7 +106,8 @@ public AudioQuery createAudioQuery(String text, int styleId) { * @return {@link AccentPhrase} のリスト。 */ @Nonnull - public List createAccentPhrasesFromKana(String kana, int styleId) { + public List createAccentPhrasesFromKana(String kana, int styleId) + throws InferenceFailedException { String accentPhrasesJson = rsAccentPhrasesFromKana(kana, styleId); Gson gson = new Gson(); AccentPhrase[] rawAccentPhrases = gson.fromJson(accentPhrasesJson, AccentPhrase[].class); @@ -121,7 +125,8 @@ public List createAccentPhrasesFromKana(String kana, int styleId) * @return {@link AccentPhrase} のリスト。 */ @Nonnull - public List createAccentPhrases(String text, int styleId) { + public List createAccentPhrases(String text, int styleId) + throws InferenceFailedException { String accentPhrasesJson = rsAccentPhrases(text, styleId); Gson gson = new Gson(); AccentPhrase[] rawAccentPhrases = gson.fromJson(accentPhrasesJson, AccentPhrase[].class); @@ -139,7 +144,8 @@ public List createAccentPhrases(String text, int styleId) { * @return 変更後のアクセント句の配列。 */ @Nonnull - public List replaceMoraData(List accentPhrases, int styleId) { + public List replaceMoraData(List accentPhrases, int styleId) + throws InferenceFailedException { if (!Utils.isU32(styleId)) { throw new IllegalArgumentException("styleId"); } @@ -157,7 +163,8 @@ public List replaceMoraData(List accentPhrases, int * @return 変更後のアクセント句の配列。 */ @Nonnull - public List replacePhonemeLength(List accentPhrases, int styleId) { + public List replacePhonemeLength(List accentPhrases, int styleId) + throws InferenceFailedException { if (!Utils.isU32(styleId)) { throw new IllegalArgumentException("styleId"); } @@ -175,7 +182,8 @@ public List replacePhonemeLength(List accentPhrases, * @return 変更後のアクセント句の配列。 */ @Nonnull - public List replaceMoraPitch(List accentPhrases, int styleId) { + public List replaceMoraPitch(List accentPhrases, int styleId) + throws InferenceFailedException { if (!Utils.isU32(styleId)) { throw new IllegalArgumentException("styleId"); } @@ -226,42 +234,50 @@ public TtsConfigurator tts(String text, int styleId) { private native void rsNewWithInitialize(OpenJtalk openJtalk, Builder builder); - private native void rsLoadVoiceModel(VoiceModel voiceModel); + private native void rsLoadVoiceModel(VoiceModel voiceModel) throws InvalidModelDataException; private native void rsUnloadVoiceModel(String voiceModelId); private native boolean rsIsLoadedVoiceModel(String voiceModelId); @Nonnull - private native String rsAudioQueryFromKana(String kana, int styleId); + private native String rsAudioQueryFromKana(String kana, int styleId) + throws InferenceFailedException; @Nonnull - private native String rsAudioQuery(String text, int styleId); + private native String rsAudioQuery(String text, int styleId) throws InferenceFailedException; @Nonnull - private native String rsAccentPhrasesFromKana(String kana, int styleId); + private native String rsAccentPhrasesFromKana(String kana, int styleId) + throws InferenceFailedException; @Nonnull - private native String rsAccentPhrases(String text, int styleId); + private native String rsAccentPhrases(String text, int styleId) throws InferenceFailedException; @Nonnull - private native String rsReplaceMoraData(String accentPhrasesJson, int styleId, boolean kana); + private native String rsReplaceMoraData(String accentPhrasesJson, int styleId, boolean kana) + throws InferenceFailedException; @Nonnull - private native String rsReplacePhonemeLength(String accentPhrasesJson, int styleId, boolean kana); + private native String rsReplacePhonemeLength(String accentPhrasesJson, int styleId, boolean kana) + throws InferenceFailedException; @Nonnull - private native String rsReplaceMoraPitch(String accentPhrasesJson, int styleId, boolean kana); + private native String rsReplaceMoraPitch(String accentPhrasesJson, int styleId, boolean kana) + throws InferenceFailedException; @Nonnull private native byte[] rsSynthesis( - String queryJson, int styleId, boolean enableInterrogativeUpspeak); + String queryJson, int styleId, boolean enableInterrogativeUpspeak) + throws InferenceFailedException; @Nonnull - private native byte[] rsTtsFromKana(String kana, int styleId, boolean enableInterrogativeUpspeak); + private native byte[] rsTtsFromKana(String kana, int styleId, boolean enableInterrogativeUpspeak) + throws InferenceFailedException; @Nonnull - private native byte[] rsTts(String text, int styleId, boolean enableInterrogativeUpspeak); + private native byte[] rsTts(String text, int styleId, boolean enableInterrogativeUpspeak) + throws InferenceFailedException; private native void rsDrop(); @@ -368,7 +384,7 @@ public SynthesisConfigurator interrogativeUpspeak(boolean interrogativeUpspeak) * @return 音声データ。 */ @Nonnull - public byte[] execute() { + public byte[] execute() throws InferenceFailedException { if (!Utils.isU32(styleId)) { throw new IllegalArgumentException("styleId"); } @@ -412,7 +428,7 @@ public TtsFromKanaConfigurator interrogativeUpspeak(boolean interrogativeUpspeak * @return 音声データ。 */ @Nonnull - public byte[] execute() { + public byte[] execute() throws InferenceFailedException { if (!Utils.isU32(styleId)) { throw new IllegalArgumentException("styleId"); } @@ -454,7 +470,7 @@ public TtsConfigurator interrogativeUpspeak(boolean interrogativeUpspeak) { * @return 音声データ。 */ @Nonnull - public byte[] execute() { + public byte[] execute() throws InferenceFailedException { if (!Utils.isU32(styleId)) { throw new IllegalArgumentException("styleId"); } 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 index b0b8921a2..de1e612be 100644 --- 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 @@ -8,6 +8,8 @@ import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import java.util.HashMap; +import jp.hiroshiba.voicevoxcore.exceptions.LoadUserDictException; +import jp.hiroshiba.voicevoxcore.exceptions.SaveUserDictException; /** ユーザー辞書。 */ public class UserDict extends Dll { @@ -73,7 +75,7 @@ public void importDict(UserDict dict) { * * @param path ユーザー辞書のパス。 */ - public void load(String path) { + public void load(String path) throws LoadUserDictException { rsLoad(path); } @@ -82,7 +84,7 @@ public void load(String path) { * * @param path ユーザー辞書のパス。 */ - public void save(String path) { + public void save(String path) throws SaveUserDictException { rsSave(path); } @@ -124,9 +126,9 @@ public HashMap toHashMap() { private native void rsImportDict(UserDict dict); - private native void rsLoad(String path); + private native void rsLoad(String path) throws LoadUserDictException; - private native void rsSave(String path); + private native void rsSave(String path) throws SaveUserDictException; @Nonnull private native String rsGetWords(); 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 deleted file mode 100644 index 3bf6f53a4..000000000 --- a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/VoicevoxException.java +++ /dev/null @@ -1,8 +0,0 @@ -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/java/jp/hiroshiba/voicevoxcore/exceptions/ExtractFullContextLabelException.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/ExtractFullContextLabelException.java new file mode 100644 index 000000000..2bae2a5e8 --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/ExtractFullContextLabelException.java @@ -0,0 +1,12 @@ +package jp.hiroshiba.voicevoxcore.exceptions; + +/** コンテキストラベル出力に失敗した。 */ +public class ExtractFullContextLabelException extends IllegalArgumentException { + public ExtractFullContextLabelException(String message) { + super(message); + } + + public ExtractFullContextLabelException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/GetSupportedDevicesException.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/GetSupportedDevicesException.java new file mode 100644 index 000000000..05037dbfe --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/GetSupportedDevicesException.java @@ -0,0 +1,14 @@ +package jp.hiroshiba.voicevoxcore.exceptions; + +import java.io.IOException; + +/** サポートされているデバイス情報取得に失敗した。 */ +public class GetSupportedDevicesException extends IOException { + public GetSupportedDevicesException(String message) { + super(message); + } + + public GetSupportedDevicesException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/GpuSupportException.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/GpuSupportException.java new file mode 100644 index 000000000..ddcb13fa0 --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/GpuSupportException.java @@ -0,0 +1,12 @@ +package jp.hiroshiba.voicevoxcore.exceptions; + +/** GPUモードがサポートされていない。 */ +public class GpuSupportException extends RuntimeException { + public GpuSupportException(String message) { + super(message); + } + + public GpuSupportException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/InferenceFailedException.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/InferenceFailedException.java new file mode 100644 index 000000000..499a530df --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/InferenceFailedException.java @@ -0,0 +1,14 @@ +package jp.hiroshiba.voicevoxcore.exceptions; + +import java.io.IOException; + +/** 推論に失敗した。 */ +public class InferenceFailedException extends IOException { + public InferenceFailedException(String message) { + super(message); + } + + public InferenceFailedException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/InvalidModelDataException.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/InvalidModelDataException.java new file mode 100644 index 000000000..8cf29cbbc --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/InvalidModelDataException.java @@ -0,0 +1,14 @@ +package jp.hiroshiba.voicevoxcore.exceptions; + +import java.io.IOException; + +/** 無効なモデルデータ。 */ +public class InvalidModelDataException extends IOException { + public InvalidModelDataException(String message) { + super(message); + } + + public InvalidModelDataException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/InvalidWordException.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/InvalidWordException.java new file mode 100644 index 000000000..a3921bfd5 --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/InvalidWordException.java @@ -0,0 +1,12 @@ +package jp.hiroshiba.voicevoxcore.exceptions; + +/** ユーザー辞書の単語のバリデーションに失敗した。 */ +public class InvalidWordException extends IllegalArgumentException { + public InvalidWordException(String message) { + super(message); + } + + public InvalidWordException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/LoadUserDictException.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/LoadUserDictException.java new file mode 100644 index 000000000..8877a1c54 --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/LoadUserDictException.java @@ -0,0 +1,14 @@ +package jp.hiroshiba.voicevoxcore.exceptions; + +import java.io.IOException; + +/** ユーザー辞書を読み込めなかった。 */ +public class LoadUserDictException extends IOException { + public LoadUserDictException(String message) { + super(message); + } + + public LoadUserDictException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/ModelAlreadyLoadedException.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/ModelAlreadyLoadedException.java new file mode 100644 index 000000000..ad3763bfd --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/ModelAlreadyLoadedException.java @@ -0,0 +1,12 @@ +package jp.hiroshiba.voicevoxcore.exceptions; + +/** すでに読み込まれている音声モデルを読み込もうとした。 */ +public class ModelAlreadyLoadedException extends IllegalStateException { + public ModelAlreadyLoadedException(String message) { + super(message); + } + + public ModelAlreadyLoadedException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/ModelNotFoundException.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/ModelNotFoundException.java new file mode 100644 index 000000000..a2f750122 --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/ModelNotFoundException.java @@ -0,0 +1,13 @@ +package jp.hiroshiba.voicevoxcore.exceptions; + +/** 音声モデルIDに対する音声モデルが見つからなかった。 */ +public class ModelNotFoundException extends IndexOutOfBoundsException { + public ModelNotFoundException(String message) { + super(message); + } + + public ModelNotFoundException(String message, Throwable cause) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/NotLoadedOpenjtalkDictException.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/NotLoadedOpenjtalkDictException.java new file mode 100644 index 000000000..3bee93b08 --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/NotLoadedOpenjtalkDictException.java @@ -0,0 +1,12 @@ +package jp.hiroshiba.voicevoxcore.exceptions; + +/** open_jtalk辞書ファイルが読み込まれていない。 */ +public class NotLoadedOpenjtalkDictException extends IllegalStateException { + public NotLoadedOpenjtalkDictException(String message) { + super(message); + } + + public NotLoadedOpenjtalkDictException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/OpenZipFileException.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/OpenZipFileException.java new file mode 100644 index 000000000..20f2e853a --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/OpenZipFileException.java @@ -0,0 +1,14 @@ +package jp.hiroshiba.voicevoxcore.exceptions; + +import java.io.IOException; + +/** ZIPファイルを開くことに失敗した。 */ +public class OpenZipFileException extends IOException { + public OpenZipFileException(String message) { + super(message); + } + + public OpenZipFileException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/ParseKanaException.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/ParseKanaException.java new file mode 100644 index 000000000..e1d47fcec --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/ParseKanaException.java @@ -0,0 +1,12 @@ +package jp.hiroshiba.voicevoxcore.exceptions; + +/** AquesTalk風記法のテキストの解析に失敗した。 */ +public class ParseKanaException extends IllegalArgumentException { + public ParseKanaException(String message) { + super(message); + } + + public ParseKanaException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/ReadZipEntryException.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/ReadZipEntryException.java new file mode 100644 index 000000000..df0d75c0d --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/ReadZipEntryException.java @@ -0,0 +1,14 @@ +package jp.hiroshiba.voicevoxcore.exceptions; + +import java.io.IOException; + +/** ZIP内のファイルが読めなかった。 */ +public class ReadZipEntryException extends IOException { + public ReadZipEntryException(String message) { + super(message); + } + + public ReadZipEntryException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/SaveUserDictException.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/SaveUserDictException.java new file mode 100644 index 000000000..f6cce2af0 --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/SaveUserDictException.java @@ -0,0 +1,14 @@ +package jp.hiroshiba.voicevoxcore.exceptions; + +import java.io.IOException; + +/** ユーザー辞書を書き込めなかった。 */ +public class SaveUserDictException extends IOException { + public SaveUserDictException(String message) { + super(message); + } + + public SaveUserDictException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/StyleAlreadyLoadedException.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/StyleAlreadyLoadedException.java new file mode 100644 index 000000000..05ac4700f --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/StyleAlreadyLoadedException.java @@ -0,0 +1,12 @@ +package jp.hiroshiba.voicevoxcore.exceptions; + +/** すでに読み込まれているスタイルを読み込もうとした。 */ +public class StyleAlreadyLoadedException extends IllegalStateException { + public StyleAlreadyLoadedException(String message) { + super(message); + } + + public StyleAlreadyLoadedException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/StyleNotFoundException.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/StyleNotFoundException.java new file mode 100644 index 000000000..f8467a019 --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/StyleNotFoundException.java @@ -0,0 +1,13 @@ +package jp.hiroshiba.voicevoxcore.exceptions; + +/** スタイルIDに対するスタイルが見つからなかった。 */ +public class StyleNotFoundException extends IndexOutOfBoundsException { + public StyleNotFoundException(String message) { + super(message); + } + + public StyleNotFoundException(String message, Throwable cause) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/UseUserDictException.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/UseUserDictException.java new file mode 100644 index 000000000..d311f4a2b --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/UseUserDictException.java @@ -0,0 +1,12 @@ +package jp.hiroshiba.voicevoxcore.exceptions; + +/** OpenJTalkのユーザー辞書の設定に失敗した。 */ +public class UseUserDictException extends RuntimeException { + public UseUserDictException(String message) { + super(message); + } + + public UseUserDictException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/WordNotFoundException.java b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/WordNotFoundException.java new file mode 100644 index 000000000..ef52f642f --- /dev/null +++ b/crates/voicevox_core_java_api/lib/src/main/java/jp/hiroshiba/voicevoxcore/exceptions/WordNotFoundException.java @@ -0,0 +1,13 @@ +package jp.hiroshiba.voicevoxcore.exceptions; + +/** ユーザー辞書に単語が見つからなかった。 */ +public class WordNotFoundException extends IndexOutOfBoundsException { + public WordNotFoundException(String message) { + super(message); + } + + public WordNotFoundException(String message, Throwable cause) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } +} 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 index f5cdaea66..efa91eed8 100644 --- 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 @@ -8,6 +8,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; +import jp.hiroshiba.voicevoxcore.exceptions.InferenceFailedException; +import jp.hiroshiba.voicevoxcore.exceptions.InvalidModelDataException; import org.junit.jupiter.api.Test; class SynthesizerTest extends TestUtils { @@ -34,7 +36,7 @@ boolean checkAllMoras( } @Test - void checkModel() { + void checkModel() throws InvalidModelDataException { VoiceModel model = loadModel(); OpenJtalk openJtalk = loadOpenJtalk(); Synthesizer synthesizer = Synthesizer.builder(openJtalk).build(); @@ -45,7 +47,7 @@ void checkModel() { } @Test - void checkAudioQuery() { + void checkAudioQuery() throws InferenceFailedException, InvalidModelDataException { VoiceModel model = loadModel(); OpenJtalk openJtalk = loadOpenJtalk(); Synthesizer synthesizer = Synthesizer.builder(openJtalk).build(); @@ -56,7 +58,7 @@ void checkAudioQuery() { } @Test - void checkAccentPhrases() { + void checkAccentPhrases() throws InferenceFailedException, InvalidModelDataException { VoiceModel model = loadModel(); OpenJtalk openJtalk = loadOpenJtalk(); Synthesizer synthesizer = Synthesizer.builder(openJtalk).build(); @@ -86,7 +88,7 @@ void checkAccentPhrases() { } @Test - void checkTts() { + void checkTts() throws InferenceFailedException, InvalidModelDataException { VoiceModel model = loadModel(); OpenJtalk openJtalk = loadOpenJtalk(); Synthesizer synthesizer = Synthesizer.builder(openJtalk).build(); 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 index 9eb1077f5..ce9b7631a 100644 --- 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 @@ -4,6 +4,9 @@ import java.nio.file.Files; import java.nio.file.Path; +import jp.hiroshiba.voicevoxcore.exceptions.InferenceFailedException; +import jp.hiroshiba.voicevoxcore.exceptions.InvalidModelDataException; +import jp.hiroshiba.voicevoxcore.exceptions.LoadUserDictException; import org.junit.jupiter.api.Test; class UserDictTest extends TestUtils { @@ -11,7 +14,8 @@ class UserDictTest extends TestUtils { // 辞書ロードのテスト。 // 辞書ロード前後でkanaが異なることを確認する @Test - void checkLoad() { + void checkLoad() + throws InferenceFailedException, InvalidModelDataException, LoadUserDictException { VoiceModel model = loadModel(); OpenJtalk openJtalk = loadOpenJtalk(); Synthesizer synthesizer = Synthesizer.builder(openJtalk).build(); diff --git a/crates/voicevox_core_java_api/src/common.rs b/crates/voicevox_core_java_api/src/common.rs index d7aeb720b..2c6847f73 100644 --- a/crates/voicevox_core_java_api/src/common.rs +++ b/crates/voicevox_core_java_api/src/common.rs @@ -1,5 +1,7 @@ -use anyhow::Result; -use jni::JNIEnv; +use std::{error::Error as _, iter}; + +use derive_more::From; +use jni::{objects::JThrowable, JNIEnv}; use once_cell::sync::Lazy; use tokio::runtime::Runtime; @@ -86,7 +88,7 @@ macro_rules! enum_object { pub fn throw_if_err(mut env: JNIEnv, fallback: T, inner: F) -> T where - F: FnOnce(&mut JNIEnv) -> Result, + F: FnOnce(&mut JNIEnv) -> Result, { match inner(&mut env) { Ok(value) => value as _, @@ -95,13 +97,130 @@ where // 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)); + macro_rules! or_panic { + ($result:expr) => { + $result.unwrap_or_else(|_| { + panic!("Failed to throw exception, original error: {error:?}") + }) + }; + } + + match &error { + JavaApiError::RustApi(error) => { + macro_rules! class { + ($($variant:ident),* $(,)?) => { + match error.kind() { + $( + voicevox_core::ErrorKind::$variant => concat!( + "jp/hiroshiba/voicevoxcore/exceptions/", + stringify!($variant), + "Exception", + ), + )* + } + }; + } + + let class = class!( + NotLoadedOpenjtalkDict, + GpuSupport, + OpenZipFile, + ReadZipEntry, + ModelAlreadyLoaded, + StyleAlreadyLoaded, + InvalidModelData, + GetSupportedDevices, + StyleNotFound, + ModelNotFound, + InferenceFailed, + ExtractFullContextLabel, + ParseKana, + LoadUserDict, + SaveUserDict, + WordNotFound, + UseUserDict, + InvalidWord, + ); + + let mut sources = + iter::successors(error.source(), |&source| source.source()) + .collect::>() + .into_iter() + .rev(); + + // FIXME: `.unwrap()`ではなく、ちゃんと`.expect()`とかを書く + + let exc = JThrowable::from(if let Some(innermost) = sources.next() { + let innermost = env + .new_object( + "java/lang/RuntimeException", + "(Ljava/lang/String;)V", + &[(&env.new_string(innermost.to_string()).unwrap()).into()], + ) + .unwrap(); + + let cause = sources.fold(innermost, |cause, source| { + env.new_object( + "java/lang/RuntimeException", + "(Ljava/lang/String;Ljava/lang/Throwable;)V", + &[ + (&env.new_string(source.to_string()).unwrap()).into(), + (&cause).into(), + ], + ) + .unwrap() + }); + + env.new_object( + class, + "(Ljava/lang/String;Ljava/lang/Throwable;)V", + &[ + (&env.new_string(error.to_string()).unwrap()).into(), + (&cause).into(), + ], + ) + .unwrap() + } else { + env.new_object( + class, + "(Ljava/lang/String;)V", + &[(&env.new_string(error.to_string()).unwrap()).into()], + ) + .unwrap() + }); + + or_panic!(env.throw(exc)); + } + JavaApiError::Jni(error) => { + or_panic!(env.throw_new("java/lang/RuntimeException", error.to_string())) + } + JavaApiError::Uuid(error) => { + or_panic!( + env.throw_new("java/lang/IllegalArgumentException", error.to_string()) + ) + } + JavaApiError::DeJson(error) => { + or_panic!( + env.throw_new("java/lang/IllegalArgumentException", error.to_string()) + ) + } + }; } fallback } } } + +#[derive(From, Debug)] +pub enum JavaApiError { + #[from] + RustApi(voicevox_core::Error), + + #[from] + Jni(jni::errors::Error), + + #[from] + Uuid(uuid::Error), + + DeJson(serde_json::Error), +} diff --git a/crates/voicevox_core_java_api/src/synthesizer.rs b/crates/voicevox_core_java_api/src/synthesizer.rs index 9f245f8f7..3ddd1b47f 100644 --- a/crates/voicevox_core_java_api/src/synthesizer.rs +++ b/crates/voicevox_core_java_api/src/synthesizer.rs @@ -1,9 +1,8 @@ use crate::{ - common::{throw_if_err, RUNTIME}, + common::{throw_if_err, JavaApiError, RUNTIME}, enum_object, object, object_type, }; -use anyhow::anyhow; use jni::{ objects::{JObject, JString}, sys::{jboolean, jint, jobject}, @@ -40,7 +39,7 @@ unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsNewWithIn } else if env.is_same_object(&acceleration_mode, gpu)? { voicevox_core::AccelerationMode::Gpu } else { - return Err(anyhow!("invalid acceleration mode".to_string(),)); + panic!("予期しない`AccelerationMode`です: {acceleration_mode:?}"); }; } let cpu_num_threads = env.get_field(&builder, "cpuNumThreads", "I")?; @@ -151,7 +150,7 @@ unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsAudioQuer )? }; - let query_json = serde_json::to_string(&audio_query)?; + let query_json = serde_json::to_string(&audio_query).expect("should not fail"); let j_audio_query = env.new_string(query_json)?; @@ -179,7 +178,7 @@ unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsAudioQuer RUNTIME.block_on(internal.audio_query(&text, voicevox_core::StyleId::new(style_id)))? }; - let query_json = serde_json::to_string(&audio_query)?; + let query_json = serde_json::to_string(&audio_query).expect("should not fail"); let j_audio_query = env.new_string(query_json)?; @@ -212,7 +211,7 @@ unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsAccentPhr )? }; - let query_json = serde_json::to_string(&accent_phrases)?; + let query_json = serde_json::to_string(&accent_phrases).expect("should not fail"); let j_accent_phrases = env.new_string(query_json)?; @@ -242,7 +241,7 @@ unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsAccentPhr )? }; - let query_json = serde_json::to_string(&accent_phrases)?; + let query_json = serde_json::to_string(&accent_phrases).expect("should not fail"); let j_accent_phrases = env.new_string(query_json)?; @@ -260,7 +259,7 @@ unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsReplaceMo 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)?; + serde_json::from_str(&accent_phrases_json).map_err(JavaApiError::DeJson)?; let style_id = style_id as u32; let internal = env @@ -274,7 +273,8 @@ unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsReplaceMo )? }; - let replaced_accent_phrases_json = serde_json::to_string(&replaced_accent_phrases)?; + let replaced_accent_phrases_json = + serde_json::to_string(&replaced_accent_phrases).expect("should not fail"); Ok(env.new_string(replaced_accent_phrases_json)?.into_raw()) }) @@ -292,7 +292,7 @@ unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsReplacePh 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)?; + serde_json::from_str(&accent_phrases_json).map_err(JavaApiError::DeJson)?; let style_id = style_id as u32; let internal = env @@ -307,7 +307,8 @@ unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsReplacePh )? }; - let replaced_accent_phrases_json = serde_json::to_string(&replaced_accent_phrases)?; + let replaced_accent_phrases_json = + serde_json::to_string(&replaced_accent_phrases).expect("should not fail"); Ok(env.new_string(replaced_accent_phrases_json)?.into_raw()) }) @@ -323,7 +324,7 @@ unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsReplaceMo 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)?; + serde_json::from_str(&accent_phrases_json).map_err(JavaApiError::DeJson)?; let style_id = style_id as u32; let internal = env @@ -337,7 +338,8 @@ unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsReplaceMo )? }; - let replaced_accent_phrases_json = serde_json::to_string(&replaced_accent_phrases)?; + let replaced_accent_phrases_json = + serde_json::to_string(&replaced_accent_phrases).expect("should not fail"); Ok(env.new_string(replaced_accent_phrases_json)?.into_raw()) }) @@ -353,7 +355,8 @@ unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_Synthesizer_rsSynthesis ) -> 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 audio_query: voicevox_core::AudioQueryModel = + serde_json::from_str(&audio_query).map_err(JavaApiError::DeJson)?; let style_id = style_id as u32; let internal = env diff --git a/crates/voicevox_core_java_api/src/user_dict.rs b/crates/voicevox_core_java_api/src/user_dict.rs index f5a79d854..e85085a34 100644 --- a/crates/voicevox_core_java_api/src/user_dict.rs +++ b/crates/voicevox_core_java_api/src/user_dict.rs @@ -4,7 +4,7 @@ use std::{ sync::{Arc, Mutex}, }; -use crate::common::throw_if_err; +use crate::common::{throw_if_err, JavaApiError}; use jni::{ objects::{JObject, JString}, sys::jobject, @@ -39,7 +39,8 @@ unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_UserDict_rsAddWord<'loc let word_json = env.get_string(&word_json)?; let word_json = &Cow::from(&word_json); - let word: voicevox_core::UserDictWord = serde_json::from_str(word_json)?; + let word: voicevox_core::UserDictWord = + serde_json::from_str(word_json).map_err(JavaApiError::DeJson)?; let uuid = { let mut internal = internal.lock().unwrap(); @@ -70,7 +71,8 @@ unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_UserDict_rsUpdateWord<' let word_json = env.get_string(&word_json)?; let word_json = &Cow::from(&word_json); - let word: voicevox_core::UserDictWord = serde_json::from_str(word_json)?; + let word: voicevox_core::UserDictWord = + serde_json::from_str(word_json).map_err(JavaApiError::DeJson)?; { let mut internal = internal.lock().unwrap(); @@ -186,7 +188,7 @@ unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_UserDict_rsGetWords<'lo let words = { let internal = internal.lock().unwrap(); - serde_json::to_string(internal.words())? + serde_json::to_string(internal.words()).expect("should not fail") }; let words = env.new_string(words)?; diff --git a/crates/voicevox_core_java_api/src/voice_model.rs b/crates/voicevox_core_java_api/src/voice_model.rs index c1cdb462c..cd971a1f7 100644 --- a/crates/voicevox_core_java_api/src/voice_model.rs +++ b/crates/voicevox_core_java_api/src/voice_model.rs @@ -54,7 +54,7 @@ unsafe extern "system" fn Java_jp_hiroshiba_voicevoxcore_VoiceModel_rsGetMetasJs .clone(); let metas = internal.metas(); - let metas_json = serde_json::to_string(&metas)?; + let metas_json = serde_json::to_string(&metas).expect("should not fail"); Ok(env.new_string(metas_json)?.into_raw()) }) } diff --git a/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py b/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py index 165770dab..6c6b15355 100644 --- a/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py +++ b/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py @@ -5,7 +5,7 @@ import conftest import pytest import pytest_asyncio -from voicevox_core import OpenJtalk, Synthesizer, VoicevoxError +from voicevox_core import OpenJtalk, Synthesizer def test_enter_returns_workable_self(synthesizer: Synthesizer) -> None: @@ -24,14 +24,14 @@ def test_closing_multiple_times_is_allowed(synthesizer: Synthesizer) -> None: def test_access_after_close_denied(synthesizer: Synthesizer) -> None: synthesizer.close() - with pytest.raises(VoicevoxError, match="^The `Synthesizer` is closed$"): + with pytest.raises(ValueError, match="^The `Synthesizer` is closed$"): _ = synthesizer.metas def test_access_after_exit_denied(synthesizer: Synthesizer) -> None: with synthesizer: pass - with pytest.raises(VoicevoxError, match="^The `Synthesizer` is closed$"): + with pytest.raises(ValueError, match="^The `Synthesizer` is closed$"): _ = synthesizer.metas diff --git a/crates/voicevox_core_python_api/python/test/test_user_dict_manipulate.py b/crates/voicevox_core_python_api/python/test/test_user_dict_manipulate.py index f7902ea94..c283b48f1 100644 --- a/crates/voicevox_core_python_api/python/test/test_user_dict_manipulate.py +++ b/crates/voicevox_core_python_api/python/test/test_user_dict_manipulate.py @@ -5,6 +5,7 @@ import tempfile from uuid import UUID +import pydantic import pytest import voicevox_core # noqa: F401 @@ -69,7 +70,7 @@ async def test_user_dict_load() -> None: assert uuid_c in dict_a.words # 単語のバリデーション - with pytest.raises(voicevox_core.VoicevoxError): + with pytest.raises(pydantic.ValidationError): dict_a.add_word( voicevox_core.UserDictWord( surface="", diff --git a/crates/voicevox_core_python_api/python/voicevox_core/__init__.py b/crates/voicevox_core_python_api/python/voicevox_core/__init__.py index ce6245bfc..abcd763fe 100644 --- a/crates/voicevox_core_python_api/python/voicevox_core/__init__.py +++ b/crates/voicevox_core_python_api/python/voicevox_core/__init__.py @@ -12,11 +12,28 @@ UserDictWordType, ) from ._rust import ( # noqa: F401 + ExtractFullContextLabelError, + GetSupportedDevicesError, + GpuSupportError, + InferenceFailedError, + InvalidModelDataError, + InvalidWordError, + LoadUserDictError, + ModelAlreadyLoadedError, + ModelNotFoundError, + NotLoadedOpenjtalkDictError, OpenJtalk, + OpenZipFileError, + ParseKanaError, + ReadZipEntryError, + SaveUserDictError, + StyleAlreadyLoadedError, + StyleNotFoundError, Synthesizer, UserDict, + UseUserDictError, VoiceModel, - VoicevoxError, + WordNotFoundError, __version__, supported_devices, ) @@ -26,15 +43,32 @@ "AccelerationMode", "AccentPhrase", "AudioQuery", + "ExtractFullContextLabelError", + "GetSupportedDevicesError", + "GpuSupportError", + "InferenceFailedError", + "InvalidModelDataError", + "InvalidWordError", + "LoadUserDictError", + "ModelAlreadyLoadedError", + "ModelNotFoundError", "Mora", + "NotLoadedOpenjtalkDictError", "OpenJtalk", + "OpenZipFileError", + "ParseKanaError", + "ReadZipEntryError", + "SaveUserDictError", "SpeakerMeta", + "StyleAlreadyLoadedError", + "StyleNotFoundError", "SupportedDevices", "Synthesizer", - "VoicevoxError", "VoiceModel", "supported_devices", + "UseUserDictError", "UserDict", "UserDictWord", "UserDictWordType", + "WordNotFoundError", ] diff --git a/crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi b/crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi index b4e44c8e2..bd110f892 100644 --- a/crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi +++ b/crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi @@ -426,8 +426,93 @@ class UserDict: """ ... -class VoicevoxError(Exception): - """VOICEVOX COREのエラー。""" +class NotLoadedOpenjtalkDictError(Exception): + """open_jtalk辞書ファイルが読み込まれていない。""" + + ... + +class GpuSupportError(Exception): + """GPUモードがサポートされていない。""" + + ... + +class OpenZipFileError(Exception): + """ZIPファイルを開くことに失敗した。""" + + ... + +class ReadZipEntryError(Exception): + """ZIP内のファイルが読めなかった。""" + + ... + +class ModelAlreadyLoadedError(Exception): + """すでに読み込まれている音声モデルを読み込もうとした。""" + + ... + +class StyleAlreadyLoadedError(Exception): + """すでに読み込まれているスタイルを読み込もうとした。""" + + ... + +class InvalidModelDataError(Exception): + """無効なモデルデータ。""" + + ... + +class GetSupportedDevicesError(Exception): + """サポートされているデバイス情報取得に失敗した。""" + + ... + +class StyleNotFoundError(KeyError): + """スタイルIDに対するスタイルが見つからなかった。""" + + ... + +class ModelNotFoundError(KeyError): + """音声モデルIDに対する音声モデルが見つからなかった。""" + + ... + +class InferenceFailedError(Exception): + """推論に失敗した。""" + + ... + +class ExtractFullContextLabelError(Exception): + """コンテキストラベル出力に失敗した。""" + + ... + +class ParseKanaError(ValueError): + """AquesTalk風記法のテキストの解析に失敗した。""" + + ... + +class LoadUserDictError(Exception): + """ユーザー辞書を読み込めなかった。""" + + ... + +class SaveUserDictError(Exception): + """ユーザー辞書を書き込めなかった。""" + + ... + +class WordNotFoundError(KeyError): + """ユーザー辞書に単語が見つからなかった。""" + + ... + +class UseUserDictError(Exception): + """OpenJTalkのユーザー辞書の設定に失敗した。""" + + ... + +class InvalidWordError(ValueError): + """ユーザー辞書の単語のバリデーションに失敗した。""" ... diff --git a/crates/voicevox_core_python_api/src/convert.rs b/crates/voicevox_core_python_api/src/convert.rs index 5eff85fb4..d4f9cb0d7 100644 --- a/crates/voicevox_core_python_api/src/convert.rs +++ b/crates/voicevox_core_python_api/src/convert.rs @@ -1,8 +1,11 @@ -use crate::VoicevoxError; -use std::{fmt::Display, future::Future, path::PathBuf}; +use std::{error::Error as _, future::Future, iter, path::PathBuf}; use easy_ext::ext; -use pyo3::{types::PyList, FromPyObject as _, PyAny, PyObject, PyResult, Python, ToPyObject}; +use pyo3::{ + exceptions::{PyException, PyValueError}, + types::PyList, + FromPyObject as _, PyAny, PyObject, PyResult, Python, ToPyObject, +}; use serde::{de::DeserializeOwned, Serialize}; use serde_json::json; use uuid::Uuid; @@ -10,6 +13,14 @@ use voicevox_core::{ AccelerationMode, AccentPhraseModel, StyleId, UserDictWordType, VoiceModelMeta, }; +use crate::{ + ExtractFullContextLabelError, GetSupportedDevicesError, GpuSupportError, InferenceFailedError, + InvalidModelDataError, InvalidWordError, LoadUserDictError, ModelAlreadyLoadedError, + ModelNotFoundError, NotLoadedOpenjtalkDictError, OpenZipFileError, ParseKanaError, + ReadZipEntryError, SaveUserDictError, StyleAlreadyLoadedError, StyleNotFoundError, + UseUserDictError, WordNotFoundError, +}; + pub fn from_acceleration_mode(ob: &PyAny) -> PyResult { let py = ob.py(); @@ -31,7 +42,7 @@ pub fn from_utf8_path(ob: &PyAny) -> PyResult { PathBuf::extract(ob)? .into_os_string() .into_string() - .map_err(|s| VoicevoxError::new_err(format!("{s:?} cannot be encoded to UTF-8"))) + .map_err(|s| PyValueError::new_err(format!("{s:?} cannot be encoded to UTF-8"))) } pub fn from_dataclass(ob: &PyAny) -> PyResult { @@ -42,7 +53,7 @@ pub fn from_dataclass(ob: &PyAny) -> PyResult { .import("json")? .call_method1("dumps", (ob,))? .extract::()?; - serde_json::from_str(json).into_py_result() + serde_json::from_str(json).into_py_value_result() } pub fn to_pydantic_voice_model_meta<'py>( @@ -63,7 +74,7 @@ pub fn to_pydantic_voice_model_meta<'py>( pub fn to_pydantic_dataclass(x: impl Serialize, class: &PyAny) -> PyResult<&PyAny> { let py = class.py(); - let x = serde_json::to_string(&x).into_py_result()?; + let x = serde_json::to_string(&x).into_py_value_result()?; let x = py.import("json")?.call_method1("loads", (x,))?.downcast()?; class.call((), Some(x)) } @@ -86,11 +97,10 @@ where py, pyo3_asyncio::tokio::get_current_locals(py)?, async move { - let replaced_accent_phrases = method(rust_accent_phrases, speaker_id) - .await - .into_py_result()?; + let replaced_accent_phrases = method(rust_accent_phrases, speaker_id).await; Python::with_gil(|py| { let replaced_accent_phrases = replaced_accent_phrases + .into_py_result(py)? .iter() .map(move |accent_phrase| { to_pydantic_dataclass( @@ -107,7 +117,7 @@ where } pub fn to_rust_uuid(ob: &PyAny) -> PyResult { let uuid = ob.getattr("hex")?.extract::()?; - uuid.parse().into_py_result() + uuid.parse::().into_py_value_result() } pub fn to_py_uuid(py: Python, uuid: Uuid) -> PyResult { let uuid = uuid.hyphenated().to_string(); @@ -122,7 +132,7 @@ pub fn to_rust_user_dict_word(ob: &PyAny) -> PyResult( py: Python<'py>, @@ -137,12 +147,65 @@ pub fn to_py_user_dict_word<'py>( pub fn to_rust_word_type(word_type: &PyAny) -> PyResult { let name = word_type.getattr("name")?.extract::()?; - serde_json::from_value::(json!(name)).into_py_result() + serde_json::from_value::(json!(name)).into_py_value_result() +} + +#[ext] +pub impl voicevox_core::Result { + fn into_py_result(self, py: Python<'_>) -> PyResult { + use voicevox_core::ErrorKind; + + self.map_err(|err| { + let msg = err.to_string(); + let top = match err.kind() { + ErrorKind::NotLoadedOpenjtalkDict => NotLoadedOpenjtalkDictError::new_err(msg), + ErrorKind::GpuSupport => GpuSupportError::new_err(msg), + ErrorKind::OpenZipFile => OpenZipFileError::new_err(msg), + ErrorKind::ReadZipEntry => ReadZipEntryError::new_err(msg), + ErrorKind::ModelAlreadyLoaded => ModelAlreadyLoadedError::new_err(msg), + ErrorKind::StyleAlreadyLoaded => StyleAlreadyLoadedError::new_err(msg), + ErrorKind::InvalidModelData => InvalidModelDataError::new_err(msg), + ErrorKind::GetSupportedDevices => GetSupportedDevicesError::new_err(msg), + ErrorKind::StyleNotFound => StyleNotFoundError::new_err(msg), + ErrorKind::ModelNotFound => ModelNotFoundError::new_err(msg), + ErrorKind::InferenceFailed => InferenceFailedError::new_err(msg), + ErrorKind::ExtractFullContextLabel => ExtractFullContextLabelError::new_err(msg), + ErrorKind::ParseKana => ParseKanaError::new_err(msg), + ErrorKind::LoadUserDict => LoadUserDictError::new_err(msg), + ErrorKind::SaveUserDict => SaveUserDictError::new_err(msg), + ErrorKind::WordNotFound => WordNotFoundError::new_err(msg), + ErrorKind::UseUserDict => UseUserDictError::new_err(msg), + ErrorKind::InvalidWord => InvalidWordError::new_err(msg), + }; + + [top] + .into_iter() + .chain( + iter::successors(err.source(), |&source| source.source()) + .map(|source| PyException::new_err(source.to_string())), + ) + .collect::>() + .into_iter() + .rev() + .reduce(|prev, source| { + source.set_cause(py, Some(prev)); + source + }) + .expect("should not be empty") + }) + } +} + +#[ext] +impl std::result::Result { + fn into_py_value_result(self) -> PyResult { + self.map_err(|e| PyValueError::new_err(e.to_string())) + } } #[ext] -pub impl Result { - fn into_py_result(self) -> PyResult { - self.map_err(|e| VoicevoxError::new_err(e.to_string())) +impl serde_json::Result { + fn into_py_value_result(self) -> PyResult { + self.map_err(|e| PyValueError::new_err(e.to_string())) } } diff --git a/crates/voicevox_core_python_api/src/lib.rs b/crates/voicevox_core_python_api/src/lib.rs index adabd09ae..1a492878e 100644 --- a/crates/voicevox_core_python_api/src/lib.rs +++ b/crates/voicevox_core_python_api/src/lib.rs @@ -6,7 +6,7 @@ use log::debug; use once_cell::sync::Lazy; use pyo3::{ create_exception, - exceptions::PyException, + exceptions::{PyException, PyKeyError, PyValueError}, pyclass, pyfunction, pymethods, pymodule, types::{IntoPyDict as _, PyBytes, PyDict, PyList, PyModule}, wrap_pyfunction, PyAny, PyObject, PyRef, PyResult, PyTypeInfo, Python, ToPyObject, @@ -22,7 +22,7 @@ static RUNTIME: Lazy = Lazy::new(|| Runtime::new().unwrap()); #[pymodule] #[pyo3(name = "_rust")] -fn rust(py: Python<'_>, module: &PyModule) -> PyResult<()> { +fn rust(_: Python<'_>, module: &PyModule) -> PyResult<()> { pyo3_log::init(); module.add("__version__", env!("CARGO_PKG_VERSION"))?; @@ -34,16 +34,45 @@ fn rust(py: Python<'_>, module: &PyModule) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; - module.add("VoicevoxError", py.get_type::())?; - Ok(()) + + add_exceptions(module) } -create_exception!( - voicevox_core, - VoicevoxError, - PyException, - "voicevox_core Error." -); +macro_rules! exceptions { + ($($name:ident: $base:ty;)*) => { + $( + create_exception!(voicevox_core, $name, $base); + )* + + fn add_exceptions(module: &PyModule) -> PyResult<()> { + $( + module.add(stringify!($name), module.py().get_type::<$name>())?; + )* + Ok(()) + } + }; +} + +exceptions! { + NotLoadedOpenjtalkDictError: PyException; + GpuSupportError: PyException; + OpenZipFileError: PyException; + ReadZipEntryError: PyException; + ModelAlreadyLoadedError: PyException; + StyleAlreadyLoadedError: PyException; + InvalidModelDataError: PyException; + GetSupportedDevicesError: PyException; + StyleNotFoundError: PyKeyError; + ModelNotFoundError: PyKeyError; + InferenceFailedError: PyException; + ExtractFullContextLabelError: PyException; + ParseKanaError: PyValueError; + LoadUserDictError: PyException; + SaveUserDictError: PyException; + WordNotFoundError: PyKeyError; + UseUserDictError: PyException; + InvalidWordError: PyValueError; +} #[pyclass] #[derive(Clone)] @@ -57,7 +86,7 @@ fn supported_devices(py: Python) -> PyResult<&PyAny> { .import("voicevox_core")? .getattr("SupportedDevices")? .downcast()?; - let s = voicevox_core::SupportedDevices::create().into_py_result()?; + let s = voicevox_core::SupportedDevices::create().into_py_result(py)?; to_pydantic_dataclass(s, class) } @@ -69,9 +98,8 @@ impl VoiceModel { #[pyo3(from_py_with = "from_utf8_path")] path: String, ) -> PyResult<&PyAny> { pyo3_asyncio::tokio::future_into_py(py, async move { - let model = voicevox_core::VoiceModel::from_path(path) - .await - .into_py_result()?; + let model = voicevox_core::VoiceModel::from_path(path).await; + let model = Python::with_gil(|py| model.into_py_result(py))?; Ok(Self { model }) }) } @@ -96,19 +124,22 @@ struct OpenJtalk { #[pymethods] impl OpenJtalk { #[new] - fn new(#[pyo3(from_py_with = "from_utf8_path")] open_jtalk_dict_dir: String) -> PyResult { + fn new( + #[pyo3(from_py_with = "from_utf8_path")] open_jtalk_dict_dir: String, + py: Python<'_>, + ) -> PyResult { Ok(Self { open_jtalk: Arc::new( voicevox_core::OpenJtalk::new_with_initialize(open_jtalk_dict_dir) - .into_py_result()?, + .into_py_result(py)?, ), }) } - fn use_user_dict(&self, user_dict: UserDict) -> PyResult<()> { + fn use_user_dict(&self, user_dict: UserDict, py: Python<'_>) -> PyResult<()> { self.open_jtalk .use_user_dict(&user_dict.dict) - .into_py_result() + .into_py_result(py) } } @@ -139,9 +170,8 @@ impl Synthesizer { cpu_num_threads, }, ) - .await - .into_py_result()? - .into(); + .await; + let synthesizer = Python::with_gil(|py| synthesizer.into_py_result(py))?.into(); Ok(Self { synthesizer: Closable::new(Arc::new(synthesizer)), }) @@ -186,20 +216,21 @@ impl Synthesizer { let model: VoiceModel = model.extract()?; let synthesizer = self.synthesizer.get()?.clone(); pyo3_asyncio::tokio::future_into_py(py, async move { - synthesizer + let synthesizer = synthesizer .lock() .await .load_voice_model(&model.model) - .await - .into_py_result() + .await; + + Python::with_gil(|py| synthesizer.into_py_result(py)) }) } - fn unload_voice_model(&mut self, voice_model_id: &str) -> PyResult<()> { + fn unload_voice_model(&mut self, voice_model_id: &str, py: Python<'_>) -> PyResult<()> { RUNTIME .block_on(self.synthesizer.get()?.lock()) .unload_voice_model(&VoiceModelId::new(voice_model_id.to_string())) - .into_py_result() + .into_py_result(py) } fn is_loaded_voice_model(&self, voice_model_id: &str) -> PyResult { @@ -224,12 +255,11 @@ impl Synthesizer { .lock() .await .audio_query_from_kana(&kana, StyleId::new(style_id)) - .await - .into_py_result()?; + .await; Python::with_gil(|py| { let class = py.import("voicevox_core")?.getattr("AudioQuery")?; - let ret = to_pydantic_dataclass(audio_query, class)?; + let ret = to_pydantic_dataclass(audio_query.into_py_result(py)?, class)?; Ok(ret.to_object(py)) }) }, @@ -247,10 +277,10 @@ impl Synthesizer { .lock() .await .audio_query(&text, StyleId::new(style_id)) - .await - .into_py_result()?; + .await; Python::with_gil(|py| { + let audio_query = audio_query.into_py_result(py)?; let class = py.import("voicevox_core")?.getattr("AudioQuery")?; let ret = to_pydantic_dataclass(audio_query, class)?; Ok(ret.to_object(py)) @@ -275,11 +305,11 @@ impl Synthesizer { .lock() .await .create_accent_phrases_from_kana(&kana, StyleId::new(style_id)) - .await - .into_py_result()?; + .await; Python::with_gil(|py| { let class = py.import("voicevox_core")?.getattr("AccentPhrase")?; let accent_phrases = accent_phrases + .into_py_result(py)? .iter() .map(|ap| to_pydantic_dataclass(ap, class)) .collect::>>(); @@ -306,11 +336,11 @@ impl Synthesizer { .lock() .await .create_accent_phrases(&text, StyleId::new(style_id)) - .await - .into_py_result()?; + .await; Python::with_gil(|py| { let class = py.import("voicevox_core")?.getattr("AccentPhrase")?; let accent_phrases = accent_phrases + .into_py_result(py)? .iter() .map(|ap| to_pydantic_dataclass(ap, class)) .collect::>>(); @@ -389,9 +419,11 @@ impl Synthesizer { enable_interrogative_upspeak, }, ) - .await - .into_py_result()?; - Python::with_gil(|py| Ok(PyBytes::new(py, &wav).to_object(py))) + .await; + Python::with_gil(|py| { + let wav = wav.into_py_result(py)?; + Ok(PyBytes::new(py, &wav).to_object(py)) + }) }, ) } @@ -422,9 +454,12 @@ impl Synthesizer { .lock() .await .tts_from_kana(&kana, style_id, &options) - .await - .into_py_result()?; - Python::with_gil(|py| Ok(PyBytes::new(py, &wav).to_object(py))) + .await; + + Python::with_gil(|py| { + let wav = wav.into_py_result(py)?; + Ok(PyBytes::new(py, &wav).to_object(py)) + }) }, ) } @@ -455,9 +490,12 @@ impl Synthesizer { .lock() .await .tts(&text, style_id, &options) - .await - .into_py_result()?; - Python::with_gil(|py| Ok(PyBytes::new(py, &wav).to_object(py))) + .await; + + Python::with_gil(|py| { + let wav = wav.into_py_result(py)?; + Ok(PyBytes::new(py, &wav).to_object(py)) + }) }, ) } @@ -488,7 +526,7 @@ impl Closable { fn get(&self) -> PyResult<&T> { match &self.content { MaybeClosed::Open(content) => Ok(content), - MaybeClosed::Closed => Err(VoicevoxError::new_err(format!( + MaybeClosed::Closed => Err(PyValueError::new_err(format!( "The `{}` is closed", C::NAME, ))), @@ -510,8 +548,8 @@ impl Drop for Closable { } #[pyfunction] -fn _validate_pronunciation(pronunciation: &str) -> PyResult<()> { - voicevox_core::__internal::validate_pronunciation(pronunciation).into_py_result() +fn _validate_pronunciation(pronunciation: &str, py: Python<'_>) -> PyResult<()> { + voicevox_core::__internal::validate_pronunciation(pronunciation).into_py_result(py) } #[pyfunction] @@ -532,12 +570,12 @@ impl UserDict { Self::default() } - fn load(&mut self, path: &str) -> PyResult<()> { - self.dict.load(path).into_py_result() + fn load(&mut self, path: &str, py: Python<'_>) -> PyResult<()> { + self.dict.load(path).into_py_result(py) } - fn save(&self, path: &str) -> PyResult<()> { - self.dict.save(path).into_py_result() + fn save(&self, path: &str, py: Python<'_>) -> PyResult<()> { + self.dict.save(path).into_py_result(py) } fn add_word( @@ -545,7 +583,7 @@ impl UserDict { #[pyo3(from_py_with = "to_rust_user_dict_word")] word: UserDictWord, py: Python, ) -> PyResult { - let uuid = self.dict.add_word(word).into_py_result()?; + let uuid = self.dict.add_word(word).into_py_result(py)?; to_py_uuid(py, uuid) } @@ -554,21 +592,23 @@ impl UserDict { &mut self, #[pyo3(from_py_with = "to_rust_uuid")] word_uuid: Uuid, #[pyo3(from_py_with = "to_rust_user_dict_word")] word: UserDictWord, + py: Python<'_>, ) -> PyResult<()> { - self.dict.update_word(word_uuid, word).into_py_result()?; + self.dict.update_word(word_uuid, word).into_py_result(py)?; Ok(()) } fn remove_word( &mut self, #[pyo3(from_py_with = "to_rust_uuid")] word_uuid: Uuid, + py: Python<'_>, ) -> PyResult<()> { - self.dict.remove_word(word_uuid).into_py_result()?; + self.dict.remove_word(word_uuid).into_py_result(py)?; Ok(()) } - fn import_dict(&mut self, other: &UserDict) -> PyResult<()> { - self.dict.import(&other.dict).into_py_result()?; + fn import_dict(&mut self, other: &UserDict, py: Python<'_>) -> PyResult<()> { + self.dict.import(&other.dict).into_py_result(py)?; Ok(()) }