Skip to content

Commit

Permalink
エラーメッセージにおけるcontextとsourceを明確に区分する (#624)
Browse files Browse the repository at this point in the history
* [skip ci] WIP

* [skip ci] 不要な`map_err`を削除

* [skip ci] 定数を関連定数に

* `UseUserDict`

* `SaveUserDict`

* `LoadUserDict`

* `ParseKana`

* `ExtractFullContextLabel`

* `GetSupportedDevices`

* `LoadModel`

* `name` → `function`
  • Loading branch information
qryxip authored Oct 3, 2023
1 parent 48a0629 commit ad2a3e9
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 112 deletions.
2 changes: 1 addition & 1 deletion crates/voicevox_core/src/devices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
66 changes: 43 additions & 23 deletions crates/voicevox_core/src/engine/full_context_label.rs
Original file line number Diff line number Diff line change
@@ -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<OpenjtalkFunctionError>,
}

#[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<Phoneme> },

#[error("invalid mora:{mora:?}")]
#[display(fmt = "invalid mora: {mora:?}")]
InvalidMora { mora: Box<Mora> },

#[error(transparent)]
OpenJtalk(#[from] open_jtalk::OpenJtalkError),
}

type Result<T> = std::result::Result<T, FullContextLabelError>;
Expand All @@ -38,18 +48,18 @@ static H1_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(/H:(\d+|xx)_)").unwrap
static I3_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(@(\d+|xx)\+)").unwrap());
static J1_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(/J:(\d+|xx)_)").unwrap());

fn string_feature_by_regex(re: &Regex, label: &str) -> Result<String> {
fn string_feature_by_regex(re: &Regex, label: &str) -> std::result::Result<String, ErrorKind> {
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<String>) -> Result<Self> {
fn from_label(label: impl Into<String>) -> std::result::Result<Self, ErrorKind> {
let mut contexts = HashMap::<String, String>::with_capacity(10);
let label = label.into();
contexts.insert("p3".into(), string_feature_by_regex(&P3_REGEX, &label)?);
Expand Down Expand Up @@ -116,7 +126,7 @@ pub struct AccentPhrase {
}

impl AccentPhrase {
pub(crate) fn from_phonemes(mut phonemes: Vec<Phoneme>) -> Result<Self> {
fn from_phonemes(mut phonemes: Vec<Phoneme>) -> std::result::Result<Self, ErrorKind> {
let mut moras = Vec::with_capacity(phonemes.len());
let mut mora_phonemes = Vec::with_capacity(phonemes.len());
for i in 0..phonemes.len() {
Expand All @@ -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();
}
Expand All @@ -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(),
})?;

Expand Down Expand Up @@ -208,7 +218,7 @@ pub struct BreathGroup {
}

impl BreathGroup {
pub(crate) fn from_phonemes(phonemes: Vec<Phoneme>) -> Result<Self> {
fn from_phonemes(phonemes: Vec<Phoneme>) -> std::result::Result<Self, ErrorKind> {
let mut accent_phrases = Vec::with_capacity(phonemes.len());
let mut accent_phonemes = Vec::with_capacity(phonemes.len());
for i in 0..phonemes.len() {
Expand Down Expand Up @@ -256,7 +266,7 @@ pub struct Utterance {
}

impl Utterance {
pub(crate) fn from_phonemes(phonemes: Vec<Phoneme>) -> Result<Self> {
fn from_phonemes(phonemes: Vec<Phoneme>) -> std::result::Result<Self, ErrorKind> {
let mut breath_groups = vec![];
let mut group_phonemes = Vec::with_capacity(phonemes.len());
let mut pauses = vec![];
Expand Down Expand Up @@ -309,12 +319,22 @@ impl Utterance {
open_jtalk: &open_jtalk::OpenJtalk,
text: impl AsRef<str>,
) -> Result<Self> {
let labels = open_jtalk.extract_fullcontext(text)?;
Self::from_phonemes(
labels
.into_iter()
.map(Phoneme::from_label)
.collect::<Result<Vec<_>>>()?,
)
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::<std::result::Result<Vec<_>, _>>()
.and_then(Self::from_phonemes)
.map_err(|context| FullContextLabelError {
context,
source: None,
})
}
}
11 changes: 2 additions & 9 deletions crates/voicevox_core/src/engine/kana_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = std::result::Result<T, KanaParseError>;

static TEXT2MORA_WITH_UNVOICE: Lazy<HashMap<String, MoraModel>> = Lazy::new(|| {
Expand Down
82 changes: 39 additions & 43 deletions crates/voicevox_core/src/engine/open_jtalk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,22 @@ use std::{
path::{Path, PathBuf},
sync::Mutex,
};

use anyhow::anyhow;
use tempfile::NamedTempFile;

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<anyhow::Error>,
},
#[error("`{function}`の実行が失敗しました")]
pub(crate) struct OpenjtalkFunctionError {
function: &'static str,
#[source]
source: Option<Text2MecabError>,
}

type Result<T> = std::result::Result<T, OpenJtalkError>;

/// テキスト解析器としてのOpen JTalk。
pub struct OpenJtalk {
resources: Mutex<Resources>,
Expand Down Expand Up @@ -53,8 +49,10 @@ impl OpenJtalk {
open_jtalk_dict_dir: impl AsRef<Path>,
) -> crate::result::Result<Self> {
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)
}

Expand All @@ -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でユーザー辞書をコンパイル
Expand All @@ -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<str>) -> Result<Vec<String>> {
pub(crate) fn extract_fullcontext(
&self,
text: impl AsRef<str>,
) -> std::result::Result<Vec<String>, OpenjtalkFunctionError> {
let Resources {
mecab,
njd,
Expand All @@ -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();
Expand All @@ -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<Path>) -> Result<()> {
fn load(&mut self, open_jtalk_dict_dir: impl AsRef<Path>) -> std::result::Result<(), ()> {
let result = self
.resources
.lock()
Expand All @@ -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(())
}
}

Expand Down Expand Up @@ -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<Vec<String>>) {
fn extract_fullcontext_works(
#[case] text: &str,
#[case] expected: std::result::Result<Vec<String>, 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);
Expand All @@ -287,7 +283,7 @@ mod tests {
#[case("こんにちは、ヒホです。", Ok(testdata_hello_hiho()))]
fn extract_fullcontext_loop_works(
#[case] text: &str,
#[case] expected: super::Result<Vec<String>>,
#[case] expected: std::result::Result<Vec<String>, OpenjtalkFunctionError>,
) {
let open_jtalk = OpenJtalk::new_with_initialize(OPEN_JTALK_DIC_DIR).unwrap();
for _ in 0..10 {
Expand Down
Loading

0 comments on commit ad2a3e9

Please sign in to comment.