diff --git a/Cargo.lock b/Cargo.lock index 6222c0f39..aba83a05e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4394,6 +4394,7 @@ dependencies = [ "serde", "serde_json", "smallvec", + "strum", "tempfile", "test_util", "thiserror", diff --git a/crates/voicevox_core/Cargo.toml b/crates/voicevox_core/Cargo.toml index 02f1860cf..e2df292fa 100644 --- a/crates/voicevox_core/Cargo.toml +++ b/crates/voicevox_core/Cargo.toml @@ -35,6 +35,7 @@ regex.workspace = true serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, features = ["preserve_order"] } smallvec.workspace = true +strum = { workspace = true, features = ["derive"] } tempfile.workspace = true thiserror.workspace = true tokio = { workspace = true, features = ["rt"] } # FIXME: feature-gateする diff --git a/crates/voicevox_core/src/error.rs b/crates/voicevox_core/src/error.rs index 19d464d21..c0445f2e0 100644 --- a/crates/voicevox_core/src/error.rs +++ b/crates/voicevox_core/src/error.rs @@ -1,7 +1,7 @@ use crate::{ engine::{FullContextLabelError, KanaParseError}, user_dict::InvalidWordError, - StyleId, VoiceModelId, + StyleId, StyleType, VoiceModelId, }; //use engine:: use duplicate::duplicate_item; @@ -38,6 +38,7 @@ impl Error { LoadModelErrorKind::ReadZipEntry { .. } => ErrorKind::ReadZipEntry, LoadModelErrorKind::ModelAlreadyLoaded { .. } => ErrorKind::ModelAlreadyLoaded, LoadModelErrorKind::StyleAlreadyLoaded { .. } => ErrorKind::StyleAlreadyLoaded, + LoadModelErrorKind::MissingModelData { .. } => ErrorKind::MissingModelData, LoadModelErrorKind::InvalidModelData => ErrorKind::InvalidModelData, }, ErrorRepr::GetSupportedDevices(_) => ErrorKind::GetSupportedDevices, @@ -121,6 +122,8 @@ pub enum ErrorKind { ModelAlreadyLoaded, /// すでに読み込まれているスタイルを読み込もうとした。 StyleAlreadyLoaded, + /// モデルデータが見つからなかった。 + MissingModelData, /// 無効なモデルデータ。 InvalidModelData, /// サポートされているデバイス情報取得に失敗した。 @@ -169,6 +172,8 @@ pub(crate) enum LoadModelErrorKind { ModelAlreadyLoaded { id: VoiceModelId }, #[display(fmt = "スタイル`{id}`は既に読み込まれています")] StyleAlreadyLoaded { id: StyleId }, + #[display(fmt = "`{style_type}`に対応するモデルデータがありませんでした")] + MissingModelData { style_type: StyleType }, #[display(fmt = "モデルデータを読むことができませんでした")] InvalidModelData, } diff --git a/crates/voicevox_core/src/infer.rs b/crates/voicevox_core/src/infer.rs index 8e3218dde..3d66e774a 100644 --- a/crates/voicevox_core/src/infer.rs +++ b/crates/voicevox_core/src/infer.rs @@ -11,7 +11,7 @@ use enum_map::{Enum, EnumMap}; use ndarray::{Array, ArrayD, Dimension, ShapeError}; use thiserror::Error; -use crate::SupportedDevices; +use crate::{StyleType, SupportedDevices}; pub(crate) trait InferenceRuntime: 'static { type Session: Sized + Send + 'static; @@ -39,6 +39,10 @@ pub(crate) trait InferenceDomainGroup { pub(crate) trait InferenceDomainMap { type Group: InferenceDomainGroup; + fn contains_for(&self, style_type: StyleType) -> bool + where + A: InferenceDomainOptionAssociation; + fn try_ref_map< F: ConvertInferenceDomainAssociationTarget, A2: InferenceDomainAssociation, @@ -66,12 +70,22 @@ pub(crate) trait InferenceDomainAssociation { type Target; } +pub(crate) trait InferenceDomainOptionAssociation: InferenceDomainAssociation { + fn is_some(x: &Self::Target) -> bool; +} + pub(crate) struct Optional(Infallible, PhantomData A>); impl InferenceDomainAssociation for Optional { type Target = Option>; } +impl InferenceDomainOptionAssociation for Optional { + fn is_some(x: &Self::Target) -> bool { + x.is_some() + } +} + /// ある`VoiceModel`が提供する推論操作の集合を示す。 pub(crate) trait InferenceDomain: Sized { type Group: InferenceDomainGroup; diff --git a/crates/voicevox_core/src/infer/domains.rs b/crates/voicevox_core/src/infer/domains.rs index f8c8af0d1..b4d3771ba 100644 --- a/crates/voicevox_core/src/infer/domains.rs +++ b/crates/voicevox_core/src/infer/domains.rs @@ -1,5 +1,7 @@ mod talk; +use crate::StyleType; + pub(crate) use self::talk::{ DecodeInput, DecodeOutput, PredictDurationInput, PredictDurationOutput, PredictIntonationInput, PredictIntonationOutput, TalkDomain, TalkOperation, @@ -7,7 +9,7 @@ pub(crate) use self::talk::{ use super::{ ConvertInferenceDomainAssociationTarget, InferenceDomainAssociation, InferenceDomainGroup, - InferenceDomainMap, + InferenceDomainMap, InferenceDomainOptionAssociation, }; pub(crate) enum InferenceDomainGroupImpl {} @@ -23,6 +25,15 @@ pub(crate) struct InferenceDomainMapImpl { impl InferenceDomainMap for InferenceDomainMapImpl { type Group = InferenceDomainGroupImpl; + fn contains_for(&self, style_type: StyleType) -> bool + where + A: InferenceDomainOptionAssociation, + { + match style_type { + StyleType::Talk => A::is_some(&self.talk), + } + } + fn try_ref_map< F: ConvertInferenceDomainAssociationTarget, A2: InferenceDomainAssociation, diff --git a/crates/voicevox_core/src/infer/status.rs b/crates/voicevox_core/src/infer/status.rs index dc9821ec4..90e5e4151 100644 --- a/crates/voicevox_core/src/infer/status.rs +++ b/crates/voicevox_core/src/infer/status.rs @@ -50,7 +50,7 @@ impl Status { self.loaded_models .lock() .unwrap() - .ensure_acceptable(model_header)?; + .ensure_acceptable(model_header, model_bytes)?; let session_set = model_bytes .try_ref_map(CreateSessionSet { @@ -131,7 +131,10 @@ impl Status { /// /// # Panics /// - /// `self`が`model_id`を含んでいないとき、パニックする。 + /// 次の場合にパニックする。 + /// + /// - `self`が`model_id`を含んでいないとき + /// - 対応する`InferenceDomain`が欠けているとき pub(crate) fn run_session( &self, model_id: &VoiceModelId, @@ -193,7 +196,10 @@ impl LoadedModels { /// # Panics /// - /// `self`が`model_id`を含んでいないとき、パニックする。 + /// 次の場合にパニックする。 + /// + /// - `self`が`model_id`を含んでいないとき + /// - 対応する`InferenceDomain`が欠けているとき fn get(&self, model_id: &VoiceModelId) -> SessionCell where I: InferenceInputSignature, @@ -201,7 +207,14 @@ impl LoadedModels { { ::Domain::visit(&self.0[model_id].session_sets) .as_ref() - .unwrap_or_else(|| todo!("`ensure_acceptable`で検査する")) + .unwrap_or_else(|| { + let type_name = + std::any::type_name::<::Domain>() + .split("::") + .last() + .unwrap(); + panic!("missing session set for `{type_name}`"); + }) .get() } @@ -219,14 +232,25 @@ impl LoadedModels { /// /// 次の場合にエラーを返す。 /// - /// - 音声モデルIDかスタイルIDが`model_header`と重複するとき - fn ensure_acceptable(&self, model_header: &VoiceModelHeader) -> LoadModelResult<()> { + /// - 現在持っている音声モデルIDかスタイルIDが`model_header`と重複するとき + /// - 必要であるはずの`InferenceDomain`のモデルデータが欠けているとき + fn ensure_acceptable( + &self, + model_header: &VoiceModelHeader, + model_bytes_or_sessions: &S::Map>, + ) -> LoadModelResult<()> { let error = |context| LoadModelError { path: model_header.path.clone(), context, source: None, }; + if self.0.contains_key(&model_header.id) { + return Err(error(LoadModelErrorKind::ModelAlreadyLoaded { + id: model_header.id.clone(), + })); + } + let loaded = self.speakers(); let external = model_header.metas.iter(); for (loaded, external) in iproduct!(loaded, external) { @@ -240,10 +264,13 @@ impl LoadedModels { .metas .iter() .flat_map(|speaker| speaker.styles()); - if self.0.contains_key(&model_header.id) { - return Err(error(LoadModelErrorKind::ModelAlreadyLoaded { - id: model_header.id.clone(), - })); + if let Some(style_type) = external + .clone() + .map(StyleMeta::r#type) + .copied() + .find(|&t| !model_bytes_or_sessions.contains_for(t)) + { + return Err(error(LoadModelErrorKind::MissingModelData { style_type })); } if let Some((style, _)) = iproduct!(loaded, external).find(|(loaded, external)| loaded.id() == external.id()) @@ -260,7 +287,7 @@ impl LoadedModels { model_header: &VoiceModelHeader, session_sets: S::Map>>, ) -> Result<()> { - self.ensure_acceptable(model_header)?; + self.ensure_acceptable(model_header, &session_sets)?; let prev = self.0.insert( model_header.id.clone(), diff --git a/crates/voicevox_core/src/metas.rs b/crates/voicevox_core/src/metas.rs index 3bdc1f277..df8f65800 100644 --- a/crates/voicevox_core/src/metas.rs +++ b/crates/voicevox_core/src/metas.rs @@ -164,7 +164,8 @@ pub struct StyleMeta { } /// **スタイル**(_style_)に対応するモデルの種類。 -#[derive(Default, Clone, Copy, Deserialize, Serialize)] +#[derive(Default, Clone, Copy, Debug, strum::Display, Deserialize, Serialize)] +#[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum StyleType { /// 音声合成クエリの作成と音声合成が可能。 diff --git a/crates/voicevox_core_c_api/src/helpers.rs b/crates/voicevox_core_c_api/src/helpers.rs index 74177db9c..c6aa3c726 100644 --- a/crates/voicevox_core_c_api/src/helpers.rs +++ b/crates/voicevox_core_c_api/src/helpers.rs @@ -39,6 +39,7 @@ pub(crate) fn into_result_code_with_error(result: CApiResult<()>) -> VoicevoxRes ReadZipEntry => VOICEVOX_RESULT_READ_ZIP_ENTRY_ERROR, ModelAlreadyLoaded => VOICEVOX_RESULT_MODEL_ALREADY_LOADED_ERROR, StyleAlreadyLoaded => VOICEVOX_RESULT_STYLE_ALREADY_LOADED_ERROR, + MissingModelData => VOICEVOX_RESULT_MISSING_MODEL_DATA_ERROR, InvalidModelData => VOICEVOX_RESULT_INVALID_MODEL_DATA_ERROR, GetSupportedDevices => VOICEVOX_RESULT_GET_SUPPORTED_DEVICES_ERROR, StyleNotFound => VOICEVOX_RESULT_STYLE_NOT_FOUND_ERROR, diff --git a/crates/voicevox_core_c_api/src/result_code.rs b/crates/voicevox_core_c_api/src/result_code.rs index 65236ada4..f8000a4dd 100644 --- a/crates/voicevox_core_c_api/src/result_code.rs +++ b/crates/voicevox_core_c_api/src/result_code.rs @@ -41,6 +41,8 @@ pub enum VoicevoxResultCode { VOICEVOX_RESULT_MODEL_ALREADY_LOADED_ERROR = 18, /// すでに読み込まれているスタイルを読み込もうとした VOICEVOX_RESULT_STYLE_ALREADY_LOADED_ERROR = 26, + /// モデルデータが見つからなかった + VOICEVOX_RESULT_MISSING_MODEL_DATA_ERROR = 28, /// 無効なモデルデータ VOICEVOX_RESULT_INVALID_MODEL_DATA_ERROR = 27, /// ユーザー辞書を読み込めなかった @@ -94,6 +96,7 @@ pub(crate) const fn error_result_to_message(result_code: VoicevoxResultCode) -> VOICEVOX_RESULT_STYLE_ALREADY_LOADED_ERROR => { cstr!("同じIDのスタイルを読むことはできません") } + VOICEVOX_RESULT_MISSING_MODEL_DATA_ERROR => cstr!("モデルデータがありませんでした"), VOICEVOX_RESULT_INVALID_MODEL_DATA_ERROR => { cstr!("モデルデータを読むことができませんでした") } diff --git a/crates/voicevox_core_java_api/src/common.rs b/crates/voicevox_core_java_api/src/common.rs index 71a60f5f3..40ed69189 100644 --- a/crates/voicevox_core_java_api/src/common.rs +++ b/crates/voicevox_core_java_api/src/common.rs @@ -71,6 +71,7 @@ where ReadZipEntry, ModelAlreadyLoaded, StyleAlreadyLoaded, + MissingModelData, InvalidModelData, GetSupportedDevices, StyleNotFound, diff --git a/crates/voicevox_core_python_api/src/convert.rs b/crates/voicevox_core_python_api/src/convert.rs index 3f629f90e..2b917d6ac 100644 --- a/crates/voicevox_core_python_api/src/convert.rs +++ b/crates/voicevox_core_python_api/src/convert.rs @@ -16,10 +16,10 @@ use voicevox_core::{ use crate::{ ExtractFullContextLabelError, GetSupportedDevicesError, GpuSupportError, InferenceFailedError, - InvalidModelDataError, InvalidWordError, LoadUserDictError, ModelAlreadyLoadedError, - ModelNotFoundError, NotLoadedOpenjtalkDictError, OpenZipFileError, ParseKanaError, - ReadZipEntryError, SaveUserDictError, StyleAlreadyLoadedError, StyleNotFoundError, - UseUserDictError, WordNotFoundError, + InvalidModelDataError, InvalidWordError, LoadUserDictError, MissingModelDataError, + ModelAlreadyLoadedError, ModelNotFoundError, NotLoadedOpenjtalkDictError, OpenZipFileError, + ParseKanaError, ReadZipEntryError, SaveUserDictError, StyleAlreadyLoadedError, + StyleNotFoundError, UseUserDictError, WordNotFoundError, }; pub(crate) fn from_acceleration_mode(ob: &PyAny) -> PyResult { @@ -194,6 +194,7 @@ pub(crate) impl voicevox_core::Result { ErrorKind::ReadZipEntry => ReadZipEntryError::new_err(msg), ErrorKind::ModelAlreadyLoaded => ModelAlreadyLoadedError::new_err(msg), ErrorKind::StyleAlreadyLoaded => StyleAlreadyLoadedError::new_err(msg), + ErrorKind::MissingModelData => MissingModelDataError::new_err(msg), ErrorKind::InvalidModelData => InvalidModelDataError::new_err(msg), ErrorKind::GetSupportedDevices => GetSupportedDevicesError::new_err(msg), ErrorKind::StyleNotFound => StyleNotFoundError::new_err(msg), diff --git a/crates/voicevox_core_python_api/src/lib.rs b/crates/voicevox_core_python_api/src/lib.rs index 4d190333d..b7387a53a 100644 --- a/crates/voicevox_core_python_api/src/lib.rs +++ b/crates/voicevox_core_python_api/src/lib.rs @@ -71,6 +71,7 @@ exceptions! { ReadZipEntryError: PyException; ModelAlreadyLoadedError: PyException; StyleAlreadyLoadedError: PyException; + MissingModelDataError: PyException; InvalidModelDataError: PyException; GetSupportedDevicesError: PyException; StyleNotFoundError: PyKeyError;