Skip to content

Commit

Permalink
IOが発生するメソッドをすべてasync化する (#667)
Browse files Browse the repository at this point in the history
* [skip ci] 空コミット

* [skip ci] IOが発生するメソッドをすべてasync化する

* テストを直す

* fixtureのスコープを指定
  • Loading branch information
qryxip authored Nov 23, 2023
1 parent fb8fc4b commit 280cc79
Show file tree
Hide file tree
Showing 14 changed files with 216 additions and 213 deletions.
2 changes: 1 addition & 1 deletion crates/voicevox_core/src/__internal/doctest_fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pub async fn synthesizer_with_sample_voice_model(
open_jtalk_dic_dir: impl AsRef<Path>,
) -> anyhow::Result<Synthesizer> {
let syntesizer = Synthesizer::new(
Arc::new(OpenJtalk::new(open_jtalk_dic_dir).unwrap()),
Arc::new(OpenJtalk::new(open_jtalk_dic_dir).await.unwrap()),
&InitializeOptions {
acceleration_mode: AccelerationMode::Cpu,
..Default::default()
Expand Down
101 changes: 61 additions & 40 deletions crates/voicevox_core/src/engine/open_jtalk.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::io::Write;
use std::sync::Arc;
use std::{
path::{Path, PathBuf},
sync::Mutex,
Expand All @@ -21,7 +22,7 @@ pub(crate) struct OpenjtalkFunctionError {

/// テキスト解析器としてのOpen JTalk。
pub struct OpenJtalk {
resources: Mutex<Resources>,
resources: Arc<Mutex<Resources>>,
dict_dir: Option<PathBuf>,
}

Expand All @@ -42,58 +43,76 @@ impl OpenJtalk {
mecab: ManagedResource::initialize(),
njd: ManagedResource::initialize(),
jpcommon: ManagedResource::initialize(),
}),
})
.into(),
dict_dir: None,
}
}
pub fn new(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(|()| {
// FIXME: 「システム辞書を読もうとしたけど読めなかった」というエラーをちゃんと用意する
ErrorRepr::NotLoadedOpenjtalkDict
})?;
Ok(s)

pub async fn new(open_jtalk_dict_dir: impl AsRef<Path>) -> crate::result::Result<Self> {
let open_jtalk_dict_dir = open_jtalk_dict_dir.as_ref().to_owned();

tokio::task::spawn_blocking(move || {
let mut s = Self::new_without_dic();
s.load(open_jtalk_dict_dir).map_err(|()| {
// FIXME: 「システム辞書を読もうとしたけど読めなかった」というエラーをちゃんと用意する
ErrorRepr::NotLoadedOpenjtalkDict
})?;
Ok(s)
})
.await
.unwrap()
}

// 先に`load`を呼ぶ必要がある。
/// ユーザー辞書を設定する。
///
/// この関数を呼び出した後にユーザー辞書を変更した場合は、再度この関数を呼ぶ必要がある。
pub fn use_user_dict(&self, user_dict: &UserDict) -> crate::result::Result<()> {
pub async fn use_user_dict(&self, user_dict: &UserDict) -> crate::result::Result<()> {
let dict_dir = self
.dict_dir
.as_ref()
.and_then(|dict_dir| dict_dir.to_str())
.ok_or(ErrorRepr::NotLoadedOpenjtalkDict)?;
.ok_or(ErrorRepr::NotLoadedOpenjtalkDict)?
.to_owned();

let resources = self.resources.clone();

let words = user_dict.to_mecab_format();

// ユーザー辞書用のcsvを作成
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.into()))?;
let temp_csv_path = temp_csv.into_temp_path();
let temp_dict = NamedTempFile::new().map_err(|e| ErrorRepr::UseUserDict(e.into()))?;
let temp_dict_path = temp_dict.into_temp_path();
let result = tokio::task::spawn_blocking(move || -> crate::Result<_> {
// ユーザー辞書用のcsvを作成
let mut temp_csv =
NamedTempFile::new().map_err(|e| ErrorRepr::UseUserDict(e.into()))?;
temp_csv
.write_all(words.as_ref())
.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.into()))?;
let temp_dict_path = temp_dict.into_temp_path();

// Mecabでユーザー辞書をコンパイル
// TODO: エラー(SEGV)が出るパターンを把握し、それをRust側で防ぐ。
mecab_dict_index(&[
"mecab-dict-index",
"-d",
dict_dir,
"-u",
temp_dict_path.to_str().unwrap(),
"-f",
"utf-8",
"-t",
"utf-8",
temp_csv_path.to_str().unwrap(),
"-q",
]);
// Mecabでユーザー辞書をコンパイル
// TODO: エラー(SEGV)が出るパターンを把握し、それをRust側で防ぐ。
mecab_dict_index(&[
"mecab-dict-index",
"-d",
&dict_dir,
"-u",
temp_dict_path.to_str().unwrap(),
"-f",
"utf-8",
"-t",
"utf-8",
temp_csv_path.to_str().unwrap(),
"-q",
]);

let Resources { mecab, .. } = &mut *self.resources.lock().unwrap();
let Resources { mecab, .. } = &mut *resources.lock().unwrap();

let result = mecab.load_with_userdic(Path::new(dict_dir), Some(Path::new(&temp_dict_path)));
Ok(mecab.load_with_userdic(dict_dir.as_ref(), Some(Path::new(&temp_dict_path))))
})
.await
.unwrap()?;

if !result {
return Err(ErrorRepr::UseUserDict(anyhow!("辞書のコンパイルに失敗しました")).into());
Expand Down Expand Up @@ -269,22 +288,24 @@ mod tests {
#[rstest]
#[case("", Err(OpenjtalkFunctionError { function: "Mecab_get_feature", source: None }))]
#[case("こんにちは、ヒホです。", Ok(testdata_hello_hiho()))]
fn extract_fullcontext_works(
#[tokio::test]
async fn extract_fullcontext_works(
#[case] text: &str,
#[case] expected: std::result::Result<Vec<String>, OpenjtalkFunctionError>,
) {
let open_jtalk = OpenJtalk::new(OPEN_JTALK_DIC_DIR).unwrap();
let open_jtalk = OpenJtalk::new(OPEN_JTALK_DIC_DIR).await.unwrap();
let result = open_jtalk.extract_fullcontext(text);
assert_debug_fmt_eq!(expected, result);
}

#[rstest]
#[case("こんにちは、ヒホです。", Ok(testdata_hello_hiho()))]
fn extract_fullcontext_loop_works(
#[tokio::test]
async fn extract_fullcontext_loop_works(
#[case] text: &str,
#[case] expected: std::result::Result<Vec<String>, OpenjtalkFunctionError>,
) {
let open_jtalk = OpenJtalk::new(OPEN_JTALK_DIC_DIR).unwrap();
let open_jtalk = OpenJtalk::new(OPEN_JTALK_DIC_DIR).await.unwrap();
for _ in 0..10 {
let result = open_jtalk.extract_fullcontext(text);
assert_debug_fmt_eq!(expected, result);
Expand Down
17 changes: 9 additions & 8 deletions crates/voicevox_core/src/synthesizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ impl Synthesizer {
///
#[cfg_attr(windows, doc = "```no_run")] // https://github.com/VOICEVOX/voicevox_core/issues/537
#[cfg_attr(not(windows), doc = "```")]
/// # fn main() -> anyhow::Result<()> {
/// # #[tokio::main]
/// # async fn main() -> anyhow::Result<()> {
/// # use test_util::OPEN_JTALK_DIC_DIR;
/// #
/// # const ACCELERATION_MODE: AccelerationMode = AccelerationMode::Cpu;
Expand All @@ -115,7 +116,7 @@ impl Synthesizer {
/// use voicevox_core::{AccelerationMode, InitializeOptions, OpenJtalk, Synthesizer};
///
/// let mut syntesizer = Synthesizer::new(
/// Arc::new(OpenJtalk::new(OPEN_JTALK_DIC_DIR).unwrap()),
/// Arc::new(OpenJtalk::new(OPEN_JTALK_DIC_DIR).await.unwrap()),
/// &InitializeOptions {
/// acceleration_mode: ACCELERATION_MODE,
/// ..Default::default()
Expand Down Expand Up @@ -1428,7 +1429,7 @@ mod tests {
#[case] expected_kana_text: &str,
) {
let syntesizer = Synthesizer::new(
Arc::new(OpenJtalk::new(OPEN_JTALK_DIC_DIR).unwrap()),
Arc::new(OpenJtalk::new(OPEN_JTALK_DIC_DIR).await.unwrap()),
&InitializeOptions {
acceleration_mode: AccelerationMode::Cpu,
..Default::default()
Expand Down Expand Up @@ -1496,7 +1497,7 @@ mod tests {
#[case] expected_text_consonant_vowel_data: &TextConsonantVowelData,
) {
let syntesizer = Synthesizer::new(
Arc::new(OpenJtalk::new(OPEN_JTALK_DIC_DIR).unwrap()),
Arc::new(OpenJtalk::new(OPEN_JTALK_DIC_DIR).await.unwrap()),
&InitializeOptions {
acceleration_mode: AccelerationMode::Cpu,
..Default::default()
Expand Down Expand Up @@ -1561,7 +1562,7 @@ mod tests {
#[tokio::test]
async fn create_accent_phrases_works_for_japanese_commas_and_periods() {
let syntesizer = Synthesizer::new(
Arc::new(OpenJtalk::new(OPEN_JTALK_DIC_DIR).unwrap()),
Arc::new(OpenJtalk::new(OPEN_JTALK_DIC_DIR).await.unwrap()),
&InitializeOptions {
acceleration_mode: AccelerationMode::Cpu,
..Default::default()
Expand Down Expand Up @@ -1620,7 +1621,7 @@ mod tests {
#[tokio::test]
async fn mora_length_works() {
let syntesizer = Synthesizer::new(
Arc::new(OpenJtalk::new(OPEN_JTALK_DIC_DIR).unwrap()),
Arc::new(OpenJtalk::new(OPEN_JTALK_DIC_DIR).await.unwrap()),
&InitializeOptions {
acceleration_mode: AccelerationMode::Cpu,
..Default::default()
Expand Down Expand Up @@ -1656,7 +1657,7 @@ mod tests {
#[tokio::test]
async fn mora_pitch_works() {
let syntesizer = Synthesizer::new(
Arc::new(OpenJtalk::new(OPEN_JTALK_DIC_DIR).unwrap()),
Arc::new(OpenJtalk::new(OPEN_JTALK_DIC_DIR).await.unwrap()),
&InitializeOptions {
acceleration_mode: AccelerationMode::Cpu,
..Default::default()
Expand Down Expand Up @@ -1688,7 +1689,7 @@ mod tests {
#[tokio::test]
async fn mora_data_works() {
let syntesizer = Synthesizer::new(
Arc::new(OpenJtalk::new(OPEN_JTALK_DIC_DIR).unwrap()),
Arc::new(OpenJtalk::new(OPEN_JTALK_DIC_DIR).await.unwrap()),
&InitializeOptions {
acceleration_mode: AccelerationMode::Cpu,
..Default::default()
Expand Down
73 changes: 45 additions & 28 deletions crates/voicevox_core/src/user_dict/dict.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use derive_getters::Getters;
use fs_err::File;
use indexmap::IndexMap;
use itertools::join;
use uuid::Uuid;
Expand All @@ -9,9 +7,9 @@ use crate::{error::ErrorRepr, Result};

/// ユーザー辞書。
/// 単語はJSONとの相互変換のために挿入された順序を保つ。
#[derive(Clone, Debug, Default, Getters)]
#[derive(Debug, Default)]
pub struct UserDict {
words: IndexMap<Uuid, UserDictWord>,
words: std::sync::Mutex<IndexMap<Uuid, UserDictWord>>,
}

impl UserDict {
Expand All @@ -20,65 +18,84 @@ impl UserDict {
Default::default()
}

pub fn to_json(&self) -> String {
serde_json::to_string(&*self.words.lock().unwrap()).expect("should not fail")
}

pub fn with_words<R>(&self, f: impl FnOnce(&IndexMap<Uuid, UserDictWord>) -> R) -> R {
f(&self.words.lock().unwrap())
}

/// ユーザー辞書をファイルから読み込む。
///
/// # Errors
///
/// ファイルが読めなかった、または内容が不正だった場合はエラーを返す。
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.into()))?;

let words: IndexMap<Uuid, UserDictWord> =
serde_json::from_reader(store_file).map_err(|e| ErrorRepr::LoadUserDict(e.into()))?;
pub async fn load(&self, store_path: &str) -> Result<()> {
let words = async {
let words = &fs_err::tokio::read(store_path).await?;
let words = serde_json::from_slice::<IndexMap<_, _>>(words)?;
Ok(words)
}
.await
.map_err(ErrorRepr::LoadUserDict)?;

self.words.extend(words);
self.words.lock().unwrap().extend(words);
Ok(())
}

/// ユーザー辞書に単語を追加する。
pub fn add_word(&mut self, word: UserDictWord) -> Result<Uuid> {
pub fn add_word(&self, word: UserDictWord) -> Result<Uuid> {
let word_uuid = Uuid::new_v4();
self.words.insert(word_uuid, word);
self.words.lock().unwrap().insert(word_uuid, word);
Ok(word_uuid)
}

/// ユーザー辞書の単語を変更する。
pub fn update_word(&mut self, word_uuid: Uuid, new_word: UserDictWord) -> Result<()> {
if !self.words.contains_key(&word_uuid) {
pub fn update_word(&self, word_uuid: Uuid, new_word: UserDictWord) -> Result<()> {
let mut words = self.words.lock().unwrap();
if !words.contains_key(&word_uuid) {
return Err(ErrorRepr::WordNotFound(word_uuid).into());
}
self.words.insert(word_uuid, new_word);
words.insert(word_uuid, new_word);
Ok(())
}

/// ユーザー辞書から単語を削除する。
pub fn remove_word(&mut self, word_uuid: Uuid) -> Result<UserDictWord> {
let Some(word) = self.words.remove(&word_uuid) else {
pub fn remove_word(&self, word_uuid: Uuid) -> Result<UserDictWord> {
let Some(word) = self.words.lock().unwrap().remove(&word_uuid) else {
return Err(ErrorRepr::WordNotFound(word_uuid).into());
};
Ok(word)
}

/// 他のユーザー辞書をインポートする。
pub fn import(&mut self, other: &Self) -> Result<()> {
for (word_uuid, word) in &other.words {
self.words.insert(*word_uuid, word.clone());
pub fn import(&self, other: &Self) -> Result<()> {
for (word_uuid, word) in &*other.words.lock().unwrap() {
self.words.lock().unwrap().insert(*word_uuid, word.clone());
}
Ok(())
}

/// ユーザー辞書を保存する。
pub fn save(&self, store_path: &str) -> Result<()> {
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.into()))?;
Ok(())
pub async fn save(&self, store_path: &str) -> Result<()> {
fs_err::tokio::write(
store_path,
serde_json::to_vec(&self.words).expect("should not fail"),
)
.await
.map_err(|e| ErrorRepr::SaveUserDict(e.into()).into())
}

/// MeCabで使用する形式に変換する。
pub(crate) fn to_mecab_format(&self) -> String {
join(self.words.values().map(UserDictWord::to_mecab_format), "\n")
join(
self.words
.lock()
.unwrap()
.values()
.map(UserDictWord::to_mecab_format),
"\n",
)
}
}
4 changes: 2 additions & 2 deletions crates/voicevox_core_c_api/src/c_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ use voicevox_core::{InitializeOptions, OpenJtalk, Result, Synthesizer, VoiceMode
use crate::{CApiResult, OpenJtalkRc, VoicevoxSynthesizer, VoicevoxVoiceModel};

impl OpenJtalkRc {
pub(crate) fn new(open_jtalk_dic_dir: impl AsRef<Path>) -> Result<Self> {
pub(crate) async fn new(open_jtalk_dic_dir: impl AsRef<Path>) -> Result<Self> {
Ok(Self {
open_jtalk: Arc::new(OpenJtalk::new(open_jtalk_dic_dir)?),
open_jtalk: Arc::new(OpenJtalk::new(open_jtalk_dic_dir).await?),
})
}
}
Expand Down
Loading

0 comments on commit 280cc79

Please sign in to comment.