diff --git a/crates/voicevox_core/src/devices.rs b/crates/voicevox_core/src/devices.rs index 673144881..70847cb81 100644 --- a/crates/voicevox_core/src/devices.rs +++ b/crates/voicevox_core/src/devices.rs @@ -45,7 +45,7 @@ impl SupportedDevices { let mut cuda_support = false; let mut dml_support = false; for provider in onnxruntime::session::get_available_providers() - .map_err(|e| ErrorRepr::GetSupportedDevices(e.into()))? + .map_err(ErrorRepr::GetSupportedDevices)? .iter() { match provider.as_str() { diff --git a/crates/voicevox_core/src/engine/full_context_label.rs b/crates/voicevox_core/src/engine/full_context_label.rs index 31471c004..667dbc8b1 100644 --- a/crates/voicevox_core/src/engine/full_context_label.rs +++ b/crates/voicevox_core/src/engine/full_context_label.rs @@ -1,22 +1,32 @@ use std::collections::HashMap; use super::*; +use crate::engine::open_jtalk::OpenjtalkFunctionError; use once_cell::sync::Lazy; use regex::Regex; +// FIXME: 入力テキストをここで持って、メッセージに含む #[derive(thiserror::Error, Debug)] -pub(crate) enum FullContextLabelError { - #[error("label parse error label:{label}")] +#[error("入力テキストからのフルコンテキストラベル抽出に失敗しました: {context}")] +pub(crate) struct FullContextLabelError { + context: ErrorKind, + #[source] + source: Option, +} + +#[derive(derive_more::Display, Debug)] +enum ErrorKind { + #[display(fmt = "Open JTalkで解釈することができませんでした")] + OpenJtalk, + + #[display(fmt = "label parse error label: {label}")] LabelParse { label: String }, - #[error("too long mora mora_phonemes:{mora_phonemes:?}")] + #[display(fmt = "too long mora mora_phonemes: {mora_phonemes:?}")] TooLongMora { mora_phonemes: Vec }, - #[error("invalid mora:{mora:?}")] + #[display(fmt = "invalid mora: {mora:?}")] InvalidMora { mora: Box }, - - #[error(transparent)] - OpenJtalk(#[from] open_jtalk::OpenJtalkError), } type Result = std::result::Result; @@ -38,18 +48,18 @@ static H1_REGEX: Lazy = Lazy::new(|| Regex::new(r"(/H:(\d+|xx)_)").unwrap static I3_REGEX: Lazy = Lazy::new(|| Regex::new(r"(@(\d+|xx)\+)").unwrap()); static J1_REGEX: Lazy = Lazy::new(|| Regex::new(r"(/J:(\d+|xx)_)").unwrap()); -fn string_feature_by_regex(re: &Regex, label: &str) -> Result { +fn string_feature_by_regex(re: &Regex, label: &str) -> std::result::Result { if let Some(caps) = re.captures(label) { Ok(caps.get(2).unwrap().as_str().to_string()) } else { - Err(FullContextLabelError::LabelParse { + Err(ErrorKind::LabelParse { label: label.into(), }) } } impl Phoneme { - pub(crate) fn from_label(label: impl Into) -> Result { + fn from_label(label: impl Into) -> std::result::Result { let mut contexts = HashMap::::with_capacity(10); let label = label.into(); contexts.insert("p3".into(), string_feature_by_regex(&P3_REGEX, &label)?); @@ -116,7 +126,7 @@ pub struct AccentPhrase { } impl AccentPhrase { - pub(crate) fn from_phonemes(mut phonemes: Vec) -> Result { + fn from_phonemes(mut phonemes: Vec) -> std::result::Result { let mut moras = Vec::with_capacity(phonemes.len()); let mut mora_phonemes = Vec::with_capacity(phonemes.len()); for i in 0..phonemes.len() { @@ -140,7 +150,7 @@ impl AccentPhrase { mora_phonemes.get(1).unwrap().clone(), )); } else { - return Err(FullContextLabelError::TooLongMora { mora_phonemes }); + return Err(ErrorKind::TooLongMora { mora_phonemes }); } mora_phonemes.clear(); } @@ -151,11 +161,11 @@ impl AccentPhrase { .vowel() .contexts() .get("f2") - .ok_or_else(|| FullContextLabelError::InvalidMora { + .ok_or_else(|| ErrorKind::InvalidMora { mora: mora.clone().into(), })? .parse() - .map_err(|_| FullContextLabelError::InvalidMora { + .map_err(|_| ErrorKind::InvalidMora { mora: mora.clone().into(), })?; @@ -208,7 +218,7 @@ pub struct BreathGroup { } impl BreathGroup { - pub(crate) fn from_phonemes(phonemes: Vec) -> Result { + fn from_phonemes(phonemes: Vec) -> std::result::Result { let mut accent_phrases = Vec::with_capacity(phonemes.len()); let mut accent_phonemes = Vec::with_capacity(phonemes.len()); for i in 0..phonemes.len() { @@ -256,7 +266,7 @@ pub struct Utterance { } impl Utterance { - pub(crate) fn from_phonemes(phonemes: Vec) -> Result { + fn from_phonemes(phonemes: Vec) -> std::result::Result { let mut breath_groups = vec![]; let mut group_phonemes = Vec::with_capacity(phonemes.len()); let mut pauses = vec![]; @@ -309,12 +319,22 @@ impl Utterance { open_jtalk: &open_jtalk::OpenJtalk, text: impl AsRef, ) -> Result { - let labels = open_jtalk.extract_fullcontext(text)?; - Self::from_phonemes( - labels - .into_iter() - .map(Phoneme::from_label) - .collect::>>()?, - ) + let labels = + open_jtalk + .extract_fullcontext(text) + .map_err(|source| FullContextLabelError { + context: ErrorKind::OpenJtalk, + source: Some(source), + })?; + + labels + .into_iter() + .map(Phoneme::from_label) + .collect::, _>>() + .and_then(Self::from_phonemes) + .map_err(|context| FullContextLabelError { + context, + source: None, + }) } } diff --git a/crates/voicevox_core/src/engine/kana_parser.rs b/crates/voicevox_core/src/engine/kana_parser.rs index 14a3aa70d..5bd09f3e6 100644 --- a/crates/voicevox_core/src/engine/kana_parser.rs +++ b/crates/voicevox_core/src/engine/kana_parser.rs @@ -10,17 +10,10 @@ const PAUSE_DELIMITER: char = '、'; const WIDE_INTERROGATION_MARK: char = '?'; const LOOP_LIMIT: usize = 300; -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] +#[error("入力テキストをAquesTalk風記法としてパースすることに失敗しました: {_0}")] pub(crate) struct KanaParseError(String); -impl std::fmt::Display for KanaParseError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Parse Error: {}", self.0) - } -} - -impl std::error::Error for KanaParseError {} - type KanaParseResult = std::result::Result; static TEXT2MORA_WITH_UNVOICE: Lazy> = Lazy::new(|| { diff --git a/crates/voicevox_core/src/engine/open_jtalk.rs b/crates/voicevox_core/src/engine/open_jtalk.rs index b67709b08..f34b43751 100644 --- a/crates/voicevox_core/src/engine/open_jtalk.rs +++ b/crates/voicevox_core/src/engine/open_jtalk.rs @@ -3,6 +3,8 @@ use std::{ path::{Path, PathBuf}, sync::Mutex, }; + +use anyhow::anyhow; use tempfile::NamedTempFile; use ::open_jtalk::*; @@ -10,19 +12,13 @@ use ::open_jtalk::*; use crate::{error::ErrorRepr, UserDict}; #[derive(thiserror::Error, Debug)] -pub(crate) enum OpenJtalkError { - #[error("open_jtalk load error")] - Load { mecab_dict_dir: PathBuf }, - #[error("open_jtalk extract_fullcontext error")] - ExtractFullContext { - text: String, - #[source] - source: Option, - }, +#[error("`{function}`の実行が失敗しました")] +pub(crate) struct OpenjtalkFunctionError { + function: &'static str, + #[source] + source: Option, } -type Result = std::result::Result; - /// テキスト解析器としてのOpen JTalk。 pub struct OpenJtalk { resources: Mutex, @@ -53,8 +49,10 @@ impl OpenJtalk { open_jtalk_dict_dir: impl AsRef, ) -> crate::result::Result { let mut s = Self::new_without_dic(); - s.load(open_jtalk_dict_dir) - .map_err(|_| ErrorRepr::NotLoadedOpenjtalkDict)?; + s.load(open_jtalk_dict_dir).map_err(|()| { + // FIXME: 「システム辞書を読もうとしたけど読めなかった」というエラーをちゃんと用意する + ErrorRepr::NotLoadedOpenjtalkDict + })?; Ok(s) } @@ -70,13 +68,12 @@ impl OpenJtalk { .ok_or(ErrorRepr::NotLoadedOpenjtalkDict)?; // ユーザー辞書用のcsvを作成 - let mut temp_csv = - NamedTempFile::new().map_err(|e| ErrorRepr::UseUserDict(e.to_string()))?; + let mut temp_csv = NamedTempFile::new().map_err(|e| ErrorRepr::UseUserDict(e.into()))?; temp_csv .write_all(user_dict.to_mecab_format().as_bytes()) - .map_err(|e| ErrorRepr::UseUserDict(e.to_string()))?; + .map_err(|e| ErrorRepr::UseUserDict(e.into()))?; let temp_csv_path = temp_csv.into_temp_path(); - let temp_dict = NamedTempFile::new().map_err(|e| ErrorRepr::UseUserDict(e.to_string()))?; + let temp_dict = NamedTempFile::new().map_err(|e| ErrorRepr::UseUserDict(e.into()))?; let temp_dict_path = temp_dict.into_temp_path(); // Mecabでユーザー辞書をコンパイル @@ -100,15 +97,16 @@ impl OpenJtalk { let result = mecab.load_with_userdic(Path::new(dict_dir), Some(Path::new(&temp_dict_path))); if !result { - return Err( - ErrorRepr::UseUserDict("辞書のコンパイルに失敗しました".to_string()).into(), - ); + return Err(ErrorRepr::UseUserDict(anyhow!("辞書のコンパイルに失敗しました")).into()); } Ok(()) } - pub(crate) fn extract_fullcontext(&self, text: impl AsRef) -> Result> { + pub(crate) fn extract_fullcontext( + &self, + text: impl AsRef, + ) -> std::result::Result, OpenjtalkFunctionError> { let Resources { mecab, njd, @@ -119,19 +117,16 @@ impl OpenJtalk { njd.refresh(); mecab.refresh(); - let mecab_text = - text2mecab(text.as_ref()).map_err(|e| OpenJtalkError::ExtractFullContext { - text: text.as_ref().into(), - source: Some(e.into()), - })?; + let mecab_text = text2mecab(text.as_ref()).map_err(|e| OpenjtalkFunctionError { + function: "text2mecab", + source: Some(e), + })?; if mecab.analysis(mecab_text) { njd.mecab2njd( - mecab - .get_feature() - .ok_or(OpenJtalkError::ExtractFullContext { - text: text.as_ref().into(), - source: None, - })?, + mecab.get_feature().ok_or(OpenjtalkFunctionError { + function: "Mecab_get_feature", + source: None, + })?, mecab.get_size(), ); njd.set_pronunciation(); @@ -144,20 +139,20 @@ impl OpenJtalk { jpcommon.make_label(); jpcommon .get_label_feature_to_iter() - .ok_or_else(|| OpenJtalkError::ExtractFullContext { - text: text.as_ref().into(), + .ok_or(OpenjtalkFunctionError { + function: "JPCommon_get_label_feature", source: None, }) .map(|iter| iter.map(|s| s.to_string()).collect()) } else { - Err(OpenJtalkError::ExtractFullContext { - text: text.as_ref().into(), + Err(OpenjtalkFunctionError { + function: "Mecab_analysis", source: None, }) } } - fn load(&mut self, open_jtalk_dict_dir: impl AsRef) -> Result<()> { + fn load(&mut self, open_jtalk_dict_dir: impl AsRef) -> std::result::Result<(), ()> { let result = self .resources .lock() @@ -169,9 +164,7 @@ impl OpenJtalk { Ok(()) } else { self.dict_dir = None; - Err(OpenJtalkError::Load { - mecab_dict_dir: open_jtalk_dict_dir.as_ref().into(), - }) + Err(()) } } @@ -275,9 +268,12 @@ mod tests { } #[rstest] - #[case("",Err(OpenJtalkError::ExtractFullContext{text:"".into(),source:None}))] + #[case("", Err(OpenjtalkFunctionError { function: "Mecab_get_feature", source: None }))] #[case("こんにちは、ヒホです。", Ok(testdata_hello_hiho()))] - fn extract_fullcontext_works(#[case] text: &str, #[case] expected: super::Result>) { + fn extract_fullcontext_works( + #[case] text: &str, + #[case] expected: std::result::Result, OpenjtalkFunctionError>, + ) { let open_jtalk = OpenJtalk::new_with_initialize(OPEN_JTALK_DIC_DIR).unwrap(); let result = open_jtalk.extract_fullcontext(text); assert_debug_fmt_eq!(expected, result); @@ -287,7 +283,7 @@ mod tests { #[case("こんにちは、ヒホです。", Ok(testdata_hello_hiho()))] fn extract_fullcontext_loop_works( #[case] text: &str, - #[case] expected: super::Result>, + #[case] expected: std::result::Result, OpenjtalkFunctionError>, ) { let open_jtalk = OpenJtalk::new_with_initialize(OPEN_JTALK_DIC_DIR).unwrap(); for _ in 0..10 { diff --git a/crates/voicevox_core/src/error.rs b/crates/voicevox_core/src/error.rs index ce4a90b33..551bd8c52 100644 --- a/crates/voicevox_core/src/error.rs +++ b/crates/voicevox_core/src/error.rs @@ -2,6 +2,7 @@ use self::engine::{FullContextLabelError, KanaParseError}; use super::*; //use engine:: use duplicate::duplicate_item; +use onnxruntime::OrtError; use std::path::PathBuf; use thiserror::Error; use uuid::Uuid; @@ -16,6 +17,7 @@ pub struct Error(#[from] ErrorRepr); [ LoadModelError ]; [ FullContextLabelError ]; [ KanaParseError ]; + [ InvalidWordError ]; )] impl From for Error { fn from(err: E) -> Self { @@ -23,13 +25,6 @@ impl From for Error { } } -// FIXME: `ErrorRepr::InvalidWord`を`#[error(transparent)]`にする -impl From for Error { - fn from(err: InvalidWordError) -> Self { - ErrorRepr::InvalidWord(err).into() - } -} - impl Error { /// 対応する[`ErrorKind`]を返す。 pub fn kind(&self) -> ErrorKind { @@ -69,8 +64,8 @@ pub(crate) enum ErrorRepr { #[error(transparent)] LoadModel(#[from] LoadModelError), - #[error("サポートされているデバイス情報取得中にエラーが発生しました,{0}")] - GetSupportedDevices(#[source] anyhow::Error), + #[error("サポートされているデバイス情報取得中にエラーが発生しました")] + GetSupportedDevices(#[source] OrtError), #[error( "`{style_id}`に対するスタイルが見つかりませんでした。音声モデルが読み込まれていないか、読\ @@ -87,26 +82,26 @@ pub(crate) enum ErrorRepr { #[error("推論に失敗しました")] InferenceFailed, - #[error("入力テキストからのフルコンテキストラベル抽出に失敗しました,{0}")] + #[error(transparent)] ExtractFullContextLabel(#[from] FullContextLabelError), - #[error("入力テキストをAquesTalk風記法としてパースすることに失敗しました,{0}")] + #[error(transparent)] ParseKana(#[from] KanaParseError), - #[error("ユーザー辞書を読み込めませんでした: {0}")] - LoadUserDict(String), + #[error("ユーザー辞書を読み込めませんでした")] + LoadUserDict(#[source] anyhow::Error), - #[error("ユーザー辞書を書き込めませんでした: {0}")] - SaveUserDict(String), + #[error("ユーザー辞書を書き込めませんでした")] + SaveUserDict(#[source] anyhow::Error), #[error("ユーザー辞書に単語が見つかりませんでした: {0}")] WordNotFound(Uuid), - #[error("OpenJTalkのユーザー辞書の設定に失敗しました: {0}")] - UseUserDict(String), + #[error("OpenJTalkのユーザー辞書の設定に失敗しました")] + UseUserDict(#[source] anyhow::Error), - #[error("ユーザー辞書の単語のバリデーションに失敗しました: {0}")] - InvalidWord(InvalidWordError), + #[error(transparent)] + InvalidWord(#[from] InvalidWordError), } /// エラーの種類。 @@ -154,10 +149,7 @@ pub(crate) type LoadModelResult = std::result::Result; /// 音声モデル読み込みのエラー。 #[derive(Error, Debug)] -#[error( - "`{path}`の読み込みに失敗しました: {context}{}", - source.as_ref().map(|e| format!(": {e}")).unwrap_or_default()) -] +#[error("`{path}`の読み込みに失敗しました: {context}")] pub(crate) struct LoadModelError { pub(crate) path: PathBuf, pub(crate) context: LoadModelErrorKind, diff --git a/crates/voicevox_core/src/user_dict/dict.rs b/crates/voicevox_core/src/user_dict/dict.rs index dd342ddd0..1a7820a78 100644 --- a/crates/voicevox_core/src/user_dict/dict.rs +++ b/crates/voicevox_core/src/user_dict/dict.rs @@ -28,11 +28,10 @@ impl UserDict { pub fn load(&mut self, store_path: &str) -> Result<()> { let store_path = std::path::Path::new(store_path); - let store_file = - File::open(store_path).map_err(|e| ErrorRepr::LoadUserDict(e.to_string()))?; + let store_file = File::open(store_path).map_err(|e| ErrorRepr::LoadUserDict(e.into()))?; - let words: IndexMap = serde_json::from_reader(store_file) - .map_err(|e| ErrorRepr::LoadUserDict(e.to_string()))?; + let words: IndexMap = + serde_json::from_reader(store_file).map_err(|e| ErrorRepr::LoadUserDict(e.into()))?; self.words.extend(words); Ok(()) @@ -72,10 +71,9 @@ impl UserDict { /// ユーザー辞書を保存する。 pub fn save(&self, store_path: &str) -> Result<()> { - let mut file = - File::create(store_path).map_err(|e| ErrorRepr::SaveUserDict(e.to_string()))?; + let mut file = File::create(store_path).map_err(|e| ErrorRepr::SaveUserDict(e.into()))?; serde_json::to_writer(&mut file, &self.words) - .map_err(|e| ErrorRepr::SaveUserDict(e.to_string()))?; + .map_err(|e| ErrorRepr::SaveUserDict(e.into()))?; Ok(()) } diff --git a/crates/voicevox_core/src/user_dict/word.rs b/crates/voicevox_core/src/user_dict/word.rs index 6a43958db..dfa04b046 100644 --- a/crates/voicevox_core/src/user_dict/word.rs +++ b/crates/voicevox_core/src/user_dict/word.rs @@ -58,13 +58,24 @@ impl<'de> Deserialize<'de> for UserDictWord { #[allow(clippy::enum_variant_names)] // FIXME #[derive(thiserror::Error, Debug, PartialEq)] pub(crate) enum InvalidWordError { - #[error("無効な発音です({1}): {0:?}")] + #[error("{}: 無効な発音です({_1}): {_0:?}", Self::BASE_MSG)] InvalidPronunciation(String, &'static str), - #[error("優先度は{MIN_PRIORITY}以上{MAX_PRIORITY}以下である必要があります: {0}")] + #[error( + "{}: 優先度は{MIN_PRIORITY}以上{MAX_PRIORITY}以下である必要があります: {_0}", + Self::BASE_MSG + )] InvalidPriority(u32), - #[error("誤ったアクセント型です({1:?}の範囲から外れています): {0}")] + #[error( + "{}: 誤ったアクセント型です({1:?}の範囲から外れています): {_0}", + Self::BASE_MSG + )] InvalidAccentType(usize, RangeToInclusive), } + +impl InvalidWordError { + const BASE_MSG: &str = "ユーザー辞書の単語のバリデーションに失敗しました"; +} + type InvalidWordResult = std::result::Result; static PRONUNCIATION_REGEX: Lazy = Lazy::new(|| Regex::new(r"^[ァ-ヴー]+$").unwrap()); @@ -105,9 +116,8 @@ impl UserDictWord { if MIN_PRIORITY > priority || priority > MAX_PRIORITY { return Err(ErrorRepr::InvalidWord(InvalidWordError::InvalidPriority(priority)).into()); } - validate_pronunciation(&pronunciation).map_err(ErrorRepr::InvalidWord)?; - let mora_count = - calculate_mora_count(&pronunciation, accent_type).map_err(ErrorRepr::InvalidWord)?; + validate_pronunciation(&pronunciation)?; + let mora_count = calculate_mora_count(&pronunciation, accent_type)?; Ok(Self { surface: to_zenkaku(surface), pronunciation,