Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

エラーメッセージにおけるcontextとsourceを明確に区分する #624

Merged
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で解釈することができませんでした")]
Copy link
Member

Choose a reason for hiding this comment

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

めちゃめちゃ細かいですが、意外と解釈という言葉はドキュメントに出てこない気がしました。
正確にはメモリ足りなかったとかもあり得る気がするので、処理とかで良いかも。
(まあ解釈でも別にいいかな、くらいの温度感です)

Copy link
Member Author

@qryxip qryxip Oct 3, 2023

Choose a reason for hiding this comment

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

"could not interpret"のような文であれば世に溢れていると思ってました。

OOMについては... そもそもOpen JTalkは考慮しているんでしょうか? なんかC++の例外とかがそのまま発射されそうな気も... (ちなみにそんなことがもし起きた場合、Rust的にはUB)

Copy link
Member

Choose a reason for hiding this comment

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

あ、OOMは例えでした。
extract_fullcontext関数が失敗した==解釈に失敗した、ではない可能性があるなと思った次第です。
これが真かどうかはOpenJTalkAPIのドキュメントがないのでコードの深いとこまで読まないとわからないだろうから、含みを持たせた方が正確かなぁくらい。

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 },
Comment on lines -14 to -15
Copy link
Member Author

@qryxip qryxip Oct 1, 2023

Choose a reason for hiding this comment

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

ここの数十行下にFIXMEとして書いたが、このLoadは元々機能しておらず、システム辞書の読み込み失敗はNotLoadedOpenjtalkDictに統合されていた。

#[error("open_jtalk extract_fullcontext error")]
ExtractFullContext {
text: String,
#[source]
source: Option<anyhow::Error>,
},
#[error("`{name}`の実行が失敗しました")]
pub(crate) struct OpenjtalkFunctionError {
name: &'static str,
Hiroshiba marked this conversation as resolved.
Show resolved Hide resolved
#[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 {
name: "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 {
name: "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 {
name: "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 {
name: "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(())
Hiroshiba marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -275,9 +268,12 @@ mod tests {
}

#[rstest]
#[case("",Err(OpenJtalkError::ExtractFullContext{text:"".into(),source:None}))]
#[case("", Err(OpenjtalkFunctionError { name: "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