diff --git "a/docs/\350\250\255\350\250\210\346\226\271\351\207\235.md" "b/docs/\350\250\255\350\250\210\346\226\271\351\207\235.md" index 283a43201d..d76694bef8 100644 --- "a/docs/\350\250\255\350\250\210\346\226\271\351\207\235.md" +++ "b/docs/\350\250\255\350\250\210\346\226\271\351\207\235.md" @@ -3,3 +3,18 @@ ダイアログは外部 API とみなし、Vuex の Store の Action 内から呼ぶことを許容しています。 これは、ダイアログの結果によってダイアログ前後の挙動を 1 つの Action に書けるという利便性を取ったためです。 ダイアログの種類によっては View に直接作用してダイアログ UI を表示するものもありますが、これも許容することにしています。 + +## EngineId、SpeakerId、StyleId + +EngineId はエンジンが持つ ID で、世界で唯一かつ不変です。 +SpeakerId は話者が持つ ID で、世界で唯一かつ不変。エンジン間でも同じ ID を持ちます。 +StyleId はスタイルごとの ID で、話者ごとに唯一であれば良いです。 + +声を一意に決めるには、(EngineId, SpeakerId, StyleId)の3組が揃っている必要がある、という仕様を目指しています。 +現状は、音声合成APIに SpeakerId 引数が無いため、(EngineId, StyleId)の2組で一意に声が決まっています。 + +VOICEVOX は歴史的経緯により、 SpeakerId と StyleId が混同していることがあります。 +特に型が整数値になっている SpeakerId は StyleId と混同している場合があります。 +(エンジン API の SpeakerId 引数に StyleId を渡したりなど。) + +StyleId は現在整数値型になっていますが、将来的にはUuidにしたいと考えています。 diff --git a/src/components/AudioCell.vue b/src/components/AudioCell.vue index dd821707a3..be63f3063b 100644 --- a/src/components/AudioCell.vue +++ b/src/components/AudioCell.vue @@ -42,6 +42,7 @@ " @click=" changeStyleId( + characterInfo.metas.speakerUuid, getDefaultStyle(characterInfo.metas.speakerUuid).styleId ) " @@ -97,7 +98,12 @@ v-close-popup active-class="selected-character-item" :active="style.styleId === selectedStyle.styleId" - @click="changeStyleId(style.styleId)" + @click=" + changeStyleId( + characterInfo.metas.speakerUuid, + style.styleId + ) + " > userOrderedCharacterInfos.value !== undefined && + audioItem.value.engineId !== undefined && audioItem.value.styleId !== undefined - ? userOrderedCharacterInfos.value.find((info) => - info.metas.styles.find( - (style) => style.styleId === audioItem.value.styleId - ) + ? store.getters.CHARACTER_INFO( + audioItem.value.engineId, + audioItem.value.styleId ) : undefined ); @@ -256,13 +262,26 @@ export default defineComponent({ } }; - const changeStyleId = (styleId: number) => { + const changeStyleId = (speakerUuid: string, styleId: number) => { + // FIXME: 同一キャラが複数エンジンにまたがっているとき、順番が先のエンジンが必ず選択される + const engineId = store.state.engineIds.find((_engineId) => + (store.state.characterInfos[_engineId] ?? []).some( + (characterInfo) => characterInfo.metas.speakerUuid === speakerUuid + ) + ); + if (engineId === undefined) + throw new Error( + `No engineId for target character style (speakerUuid == ${speakerUuid}, styleId == ${styleId})` + ); + store.dispatch("COMMAND_CHANGE_STYLE_ID", { audioKey: props.audioKey, + engineId, styleId, }); }; const getDefaultStyle = (speakerUuid: string) => { + // FIXME: 同一キャラが複数エンジンにまたがっているとき、順番が先のエンジンが必ず選択される const characterInfo = userOrderedCharacterInfos.value?.find( (info) => info.metas.speakerUuid === speakerUuid ); @@ -316,10 +335,17 @@ export default defineComponent({ await pushAudioText(); } + const engineId = audioItem.value.engineId; + if (engineId === undefined) + throw new Error("assert engineId !== undefined"); + const styleId = audioItem.value.styleId; - if (styleId == undefined) throw new Error("styleId == undefined"); + if (styleId === undefined) + throw new Error("assert styleId !== undefined"); + const audioKeys = await store.dispatch("COMMAND_PUT_TEXTS", { texts, + engineId, styleId, prevAudioKey, }); diff --git a/src/components/CharacterPortrait.vue b/src/components/CharacterPortrait.vue index cdd960012d..07233b74bc 100644 --- a/src/components/CharacterPortrait.vue +++ b/src/components/CharacterPortrait.vue @@ -19,18 +19,17 @@ export default defineComponent({ const store = useStore(); const characterInfo = computed(() => { - const characterInfos = store.state.characterInfos || []; const activeAudioKey: string | undefined = store.getters.ACTIVE_AUDIO_KEY; const audioItem = activeAudioKey ? store.state.audioItems[activeAudioKey] : undefined; + + const engineId = audioItem?.engineId; const styleId = audioItem?.styleId; - return styleId !== undefined - ? characterInfos.find((info) => - info.metas.styles.find((style) => style.styleId === styleId) - ) - : undefined; + if (engineId === undefined || styleId === undefined) return undefined; + + return store.getters.CHARACTER_INFO(engineId, styleId); }); const characterName = computed(() => { diff --git a/src/components/DictionaryManageDialog.vue b/src/components/DictionaryManageDialog.vue index 6187721414..d9bde013ac 100644 --- a/src/components/DictionaryManageDialog.vue +++ b/src/components/DictionaryManageDialog.vue @@ -263,6 +263,8 @@ export default defineComponent({ const store = useStore(); const $q = useQuasar(); + const engineIdComputed = computed(() => store.state.engineIds[0]); // TODO: 複数エンジン対応 + const dictionaryManageDialogOpenedComputed = computed({ get: () => props.modelValue, set: (val) => emit("update:modelValue", val), @@ -282,10 +284,16 @@ export default defineComponent({ }; const loadingDictProcess = async () => { + const engineId = engineIdComputed.value; + if (engineId === undefined) + throw new Error(`assert engineId !== undefined`); + loadingDict.value = true; try { userDict.value = await createUILockAction( - store.dispatch("LOAD_USER_DICT") + store.dispatch("LOAD_USER_DICT", { + engineId, + }) ); } catch { $q.dialog({ @@ -356,6 +364,10 @@ export default defineComponent({ surface.value = convertHankakuToZenkaku(text); }; const setYomi = async (text: string, changeWord?: boolean) => { + const engineId = engineIdComputed.value; + if (engineId === undefined) + throw new Error(`assert engineId !== undefined`); + // テキスト長が0の時にエラー表示にならないように、テキスト長を考慮する isOnlyHiraOrKana.value = !text.length || kanaRegex.test(text); // 読みが変更されていない場合は、アクセントフレーズに変更を加えない @@ -378,6 +390,7 @@ export default defineComponent({ await createUILockAction( store.dispatch("FETCH_ACCENT_PHRASES", { text: text + "ガ'", + engineId, styleId: styleId.value, isKana: true, }) @@ -396,12 +409,17 @@ export default defineComponent({ }; const changeAccent = async (_: number, accent: number) => { + const engineId = engineIdComputed.value; + if (engineId === undefined) + throw new Error(`assert engineId !== undefined`); + if (accentPhrase.value) { accentPhrase.value.accent = accent; accentPhrase.value = ( await createUILockAction( store.dispatch("FETCH_MORA_DATA", { accentPhrases: [accentPhrase.value], + engineId, styleId: styleId.value, }) ) @@ -413,6 +431,10 @@ export default defineComponent({ audioElem.pause(); const play = async () => { + const engineId = engineIdComputed.value; + if (engineId === undefined) + throw new Error(`assert engineId !== undefined`); + if (!accentPhrase.value) return; nowGenerating.value = true; const query: AudioQuery = { @@ -429,6 +451,7 @@ export default defineComponent({ const audioItem: AudioItem = { text: yomi.value, + engineId, styleId: styleId.value, query, }; diff --git a/src/components/LibraryPolicy.vue b/src/components/LibraryPolicy.vue index 1cde5c22a7..8eeca73f4f 100644 --- a/src/components/LibraryPolicy.vue +++ b/src/components/LibraryPolicy.vue @@ -45,7 +45,9 @@ export default defineComponent({ const store = useStore(); const md = useMarkdownIt(); - const characterInfos = computed(() => store.state.characterInfos); + const flattenCharacterInfos = computed( + () => store.getters.GET_FLATTEN_CHARACTER_INFOS + ); const convertMarkdown = (text: string) => { return md.render(text); @@ -62,7 +64,7 @@ export default defineComponent({ }; return { - characterInfos, + characterInfos: flattenCharacterInfos, convertMarkdown, selectCharacterInfIndex, detailIndex, diff --git a/src/store/audio.ts b/src/store/audio.ts index 0e776d5eb9..a5702d6ab3 100644 --- a/src/store/audio.ts +++ b/src/store/audio.ts @@ -51,6 +51,7 @@ async function generateUniqueIdAndQuery( JSON.stringify([ audioItem.text, audioQuery, + audioItem.engineId, audioItem.styleId, state.experimentalSetting.enableInterrogativeUpspeak, // このフラグが違うと、同じAudioQueryで違う音声が生成されるので追加 ]) @@ -107,34 +108,38 @@ function parseTextFile( continue; } + // FIXME: engineIdの追加 audioItems.push({ text: splitText, styleId: lastStyleId }); } return audioItems; } +// TODO: GETTERに移動する function buildFileName(state: State, audioKey: string) { const fileNamePattern = state.savingSetting.fileNamePattern; const index = state.audioKeys.indexOf(audioKey); const audioItem = state.audioItems[audioKey]; - let styleName: string | undefined = ""; - const character = state.characterInfos?.find((info) => { - const result = info.metas.styles.findIndex( - (style) => style.styleId === audioItem.styleId - ); + if (audioItem.engineId === undefined) + throw new Error("asssrt audioItem.engineId !== undefined"); + if (audioItem.styleId === undefined) + throw new Error("assert audioItem.styleId !== undefined"); - if (result > -1) { - styleName = info.metas.styles[result].styleName; - } - - return result > -1; - }); + const character = getCharacterInfo( + state, + audioItem.engineId, + audioItem.styleId + ); + if (character === undefined) + throw new Error("assert character !== undefined"); - if (character === undefined) { - throw new Error(); - } + const style = character.metas.styles.find( + (style) => style.styleId === audioItem.styleId + ); + if (style === undefined) throw new Error("assert style !== undefined"); + const styleName = style.styleName || "ノーマル"; return buildFileNameFromRawData(fileNamePattern, { characterName: character.metas.speakerName, index, @@ -160,11 +165,28 @@ function generateWriteErrorMessage(writeFileErrorResult: WriteFileErrorResult) { return `何らかの理由で失敗しました。${writeFileErrorResult.message}`; } +// TODO: GETTERに移動する。buildFileNameから参照されているので、そちらも一緒に移動する。 +export function getCharacterInfo( + state: State, + engineId: string, + styleId: number +): CharacterInfo | undefined { + const engineCharacterInfos = state.characterInfos[engineId]; + + // (engineId, styleId)で「スタイル付きキャラクター」は一意である + return engineCharacterInfos.find((characterInfo) => + characterInfo.metas.styles.some( + (characterStyle) => characterStyle.styleId === styleId + ) + ); +} + const audioBlobCache: Record = {}; const audioElements: Record = {}; export const audioStoreState: AudioStoreState = { engineStates: {}, + characterInfos: {}, audioItems: {}, audioKeys: [], audioStates: {}, @@ -218,13 +240,18 @@ export const audioStore: VoiceVoxStoreOptions< ? audioElements[state._activeAudioKey]?.currentTime : undefined; }, - USER_ORDERED_CHARACTER_INFOS: (state) => { - const characterInfos = state.characterInfos?.slice(); - return characterInfos?.sort( - (a, b) => - state.userCharacterOrder.indexOf(a.metas.speakerUuid) - - state.userCharacterOrder.indexOf(b.metas.speakerUuid) - ); + CHARACTER_INFO: (state) => (engineId, styleId) => { + return getCharacterInfo(state, engineId, styleId); + }, + USER_ORDERED_CHARACTER_INFOS: (state, getters) => { + const flattenCharacterInfos = getters.GET_FLATTEN_CHARACTER_INFOS; + return flattenCharacterInfos.length !== 0 + ? flattenCharacterInfos.sort( + (a, b) => + state.userCharacterOrder.indexOf(a.metas.speakerUuid) - + state.userCharacterOrder.indexOf(b.metas.speakerUuid) + ) + : undefined; }, }, @@ -237,9 +264,12 @@ export const audioStore: VoiceVoxStoreOptions< }, SET_CHARACTER_INFOS( state, - { characterInfos }: { characterInfos: CharacterInfo[] } + { + engineId, + characterInfos, + }: { engineId: string; characterInfos: CharacterInfo[] } ) { - state.characterInfos = characterInfos; + state.characterInfos[engineId] = characterInfos; }, SET_AUDIO_KEY_INITIALIZING_SPEAKER( state, @@ -432,8 +462,13 @@ export const audioStore: VoiceVoxStoreOptions< }, SET_AUDIO_STYLE_ID( state, - { audioKey, styleId }: { audioKey: string; styleId: number } + { + audioKey, + engineId, + styleId, + }: { audioKey: string; engineId: string; styleId: number } ) { + state.audioItems[audioKey].engineId = engineId; state.audioItems[audioKey].styleId = styleId; }, SET_ACCENT_PHRASES( @@ -569,79 +604,86 @@ export const audioStore: VoiceVoxStoreOptions< } } ), - LOAD_CHARACTER: createUILockAction(async ({ state, commit, dispatch }) => { - const engineId: string | undefined = state.engineIds[0]; // TODO: 複数エンジン対応 - if (engineId === undefined) - throw new Error(`No such engine registered: index == 0`); - - const speakers = await dispatch("INSTANTIATE_ENGINE_CONNECTOR", { - engineId, - }) - .then((instance) => instance.invoke("speakersSpeakersGet")({})) - .catch((error) => { - window.electron.logError(error, `Failed to get speakers.`); - throw error; - }); - const base64ToUrl = function (base64: string, type: string) { - const buffer = Buffer.from(base64, "base64"); - const iconBlob = new Blob([buffer.buffer], { type: type }); - return URL.createObjectURL(iconBlob); - }; - const getStyles = function (speaker: Speaker, speakerInfo: SpeakerInfo) { - const styles: StyleInfo[] = new Array(speaker.styles.length); - speaker.styles.forEach((style, i) => { - const styleInfo = speakerInfo.styleInfos.find( - (styleInfo) => style.id === styleInfo.id - ); - if (!styleInfo) - throw new Error( - `Not found the style id "${style.id}" of "${speaker.name}". ` - ); - const voiceSamples = styleInfo.voiceSamples.map((voiceSample) => { - return base64ToUrl(voiceSample, "audio/wav"); - }); - styles[i] = { - styleName: style.name, - styleId: style.id, - iconPath: base64ToUrl(styleInfo.icon, "image/png"), - voiceSamplePaths: voiceSamples, - }; - }); - return styles; - }; - const getSpeakerInfo = async function (speaker: Speaker) { - const speakerInfo = await dispatch("INSTANTIATE_ENGINE_CONNECTOR", { + LOAD_CHARACTER_ALL: createUILockAction(async ({ state, dispatch }) => { + for (const engineId of state.engineIds) { + window.electron.logInfo(`Load CharacterInfo from engine ${engineId}`); + await dispatch("LOAD_CHARACTER", { engineId }); + } + }), + LOAD_CHARACTER: createUILockAction( + async ({ commit, dispatch }, { engineId }) => { + const speakers = await dispatch("INSTANTIATE_ENGINE_CONNECTOR", { engineId, }) - .then((instance) => - instance.invoke("speakerInfoSpeakerInfoGet")({ - speakerUuid: speaker.speakerUuid, - }) - ) + .then((instance) => instance.invoke("speakersSpeakersGet")({})) .catch((error) => { window.electron.logError(error, `Failed to get speakers.`); throw error; }); - const styles = getStyles(speaker, speakerInfo); - const characterInfo: CharacterInfo = { - portraitPath: base64ToUrl(speakerInfo.portrait, "image/png"), - metas: { - speakerUuid: speaker.speakerUuid, - speakerName: speaker.name, - styles: styles, - policy: speakerInfo.policy, - }, + const base64ToUrl = function (base64: string, type: string) { + const buffer = Buffer.from(base64, "base64"); + const iconBlob = new Blob([buffer.buffer], { type: type }); + return URL.createObjectURL(iconBlob); }; - return characterInfo; - }; - const characterInfos: CharacterInfo[] = await Promise.all( - speakers.map(async (speaker) => { - return await getSpeakerInfo(speaker); - }) - ); + const getStyles = function ( + speaker: Speaker, + speakerInfo: SpeakerInfo + ) { + const styles: StyleInfo[] = new Array(speaker.styles.length); + speaker.styles.forEach((style, i) => { + const styleInfo = speakerInfo.styleInfos.find( + (styleInfo) => style.id === styleInfo.id + ); + if (!styleInfo) + throw new Error( + `Not found the style id "${style.id}" of "${speaker.name}". ` + ); + const voiceSamples = styleInfo.voiceSamples.map((voiceSample) => { + return base64ToUrl(voiceSample, "audio/wav"); + }); + styles[i] = { + styleName: style.name, + styleId: style.id, + iconPath: base64ToUrl(styleInfo.icon, "image/png"), + voiceSamplePaths: voiceSamples, + }; + }); + return styles; + }; + const getSpeakerInfo = async function (speaker: Speaker) { + const speakerInfo = await dispatch("INSTANTIATE_ENGINE_CONNECTOR", { + engineId, + }) + .then((instance) => + instance.invoke("speakerInfoSpeakerInfoGet")({ + speakerUuid: speaker.speakerUuid, + }) + ) + .catch((error) => { + window.electron.logError(error, `Failed to get speakers.`); + throw error; + }); + const styles = getStyles(speaker, speakerInfo); + const characterInfo: CharacterInfo = { + portraitPath: base64ToUrl(speakerInfo.portrait, "image/png"), + metas: { + speakerUuid: speaker.speakerUuid, + speakerName: speaker.name, + styles: styles, + policy: speakerInfo.policy, + }, + }; + return characterInfo; + }; + const characterInfos: CharacterInfo[] = await Promise.all( + speakers.map(async (speaker) => { + return await getSpeakerInfo(speaker); + }) + ); - commit("SET_CHARACTER_INFOS", { characterInfos }); - }), + commit("SET_CHARACTER_INFOS", { engineId, characterInfos }); + } + ), GENERATE_AUDIO_KEY() { const audioKey = uuidv4(); audioElements[audioKey] = new Audio(); @@ -650,11 +692,7 @@ export const audioStore: VoiceVoxStoreOptions< /** * 指定した話者(スタイルID)がエンジン側で初期化されているか */ - async IS_INITIALIZED_ENGINE_SPEAKER({ state, dispatch }, { styleId }) { - const engineId: string | undefined = state.engineIds[0]; // TODO: 複数エンジン対応, 暫定的に0番目のエンジンのみを使用する。将来的にGENERATE_AUDIO_ITEMの引数にengineId/engineIdを追加する予定 - if (engineId === undefined) - throw new Error(`No such engine registered: index == 0`); - + async IS_INITIALIZED_ENGINE_SPEAKER({ dispatch }, { engineId, styleId }) { // FIXME: なぜかbooleanではなくstringが返ってくる。 // おそらくエンジン側のresponse_modelをBaseModel継承にしないといけない。 const isInitialized: string = await dispatch( @@ -695,8 +733,9 @@ export const audioStore: VoiceVoxStoreOptions< /** * AudioItemに設定される話者(スタイルID)に対してエンジン側の初期化を行い、即座に音声合成ができるようにする。 */ - async SETUP_SPEAKER({ commit, dispatch }, { audioKey, styleId }) { + async SETUP_SPEAKER({ commit, dispatch }, { engineId, audioKey, styleId }) { const isInitialized = await dispatch("IS_INITIALIZED_ENGINE_SPEAKER", { + engineId, styleId, }); if (isInitialized) return; @@ -704,7 +743,10 @@ export const audioStore: VoiceVoxStoreOptions< commit("SET_AUDIO_KEY_INITIALIZING_SPEAKER", { audioKey, }); - await dispatch("INITIALIZE_ENGINE_SPEAKER", { styleId }).finally(() => { + await dispatch("INITIALIZE_ENGINE_SPEAKER", { + engineId, + styleId, + }).finally(() => { commit("SET_AUDIO_KEY_INITIALIZING_SPEAKER", { audioKey: undefined, }); @@ -719,6 +761,7 @@ export const audioStore: VoiceVoxStoreOptions< { state, getters, dispatch }, payload: { text?: string; + engineId?: string; styleId?: number; presetKey?: string; baseAudioItem?: AudioItem; @@ -735,10 +778,9 @@ export const audioStore: VoiceVoxStoreOptions< const text = payload.text ?? ""; - const engineId: string | undefined = state.engineIds[0]; // TODO: 複数エンジン対応, 暫定的に0番目のエンジンのみを使用する。将来的にGENERATE_AUDIO_ITEMの引数にengineId/engineIdを追加する予定 - if (engineId === undefined) - throw new Error(`No such engine registered: index == 0`); + const engineId = payload.engineId ?? state.engineIds[0]; + // FIXME: engineIdも含めて探査する const styleId = payload.styleId ?? state.defaultStyleIds[ @@ -752,12 +794,14 @@ export const audioStore: VoiceVoxStoreOptions< const query = getters.IS_ENGINE_READY(engineId) ? await dispatch("FETCH_AUDIO_QUERY", { text, + engineId, styleId, }).catch(() => undefined) : undefined; const audioItem: AudioItem = { text, + engineId, styleId, }; if (query != undefined) { @@ -833,21 +877,19 @@ export const audioStore: VoiceVoxStoreOptions< commit("SET_AUDIO_QUERY", payload); }, FETCH_ACCENT_PHRASES( - { state, dispatch }, + { dispatch }, { text, + engineId, styleId, isKana, }: { text: string; + engineId: string; styleId: number; isKana?: boolean; } ) { - const engineId: string | undefined = state.engineIds[0]; // TODO: 複数エンジン対応 - if (engineId === undefined) - throw new Error(`No such engine registered: index == 0`); - return dispatch("INSTANTIATE_ENGINE_CONNECTOR", { engineId, }) @@ -867,16 +909,13 @@ export const audioStore: VoiceVoxStoreOptions< }); }, FETCH_MORA_DATA( - { dispatch, state }, + { dispatch }, { accentPhrases, + engineId, styleId, - }: { accentPhrases: AccentPhrase[]; styleId: number } + }: { accentPhrases: AccentPhrase[]; engineId: string; styleId: number } ) { - const engineId: string | undefined = state.engineIds[0]; // TODO: 複数エンジン対応 - if (engineId === undefined) - throw new Error(`No such engine registered: index == 0`); - return dispatch("INSTANTIATE_ENGINE_CONNECTOR", { engineId, }) @@ -900,10 +939,12 @@ export const audioStore: VoiceVoxStoreOptions< { dispatch }, { accentPhrases, + engineId, styleId, copyIndexes, }: { accentPhrases: AccentPhrase[]; + engineId: string; styleId: number; copyIndexes: number[]; } @@ -912,6 +953,7 @@ export const audioStore: VoiceVoxStoreOptions< "FETCH_MORA_DATA", { accentPhrases, + engineId, styleId, } ); @@ -921,13 +963,13 @@ export const audioStore: VoiceVoxStoreOptions< return accentPhrases; }, FETCH_AUDIO_QUERY( - { dispatch, state }, - { text, styleId }: { text: string; styleId: number } + { dispatch }, + { + text, + engineId, + styleId, + }: { text: string; engineId: string; styleId: number } ) { - const engineId: string | undefined = state.engineIds[0]; // TODO: 複数エンジン対応 - if (engineId === undefined) - throw new Error(`No such engine registered: index == 0`); - return dispatch("INSTANTIATE_ENGINE_CONNECTOR", { engineId, }) @@ -1034,7 +1076,7 @@ export const audioStore: VoiceVoxStoreOptions< { dispatch, state }, { encodedBlobs }: { encodedBlobs: string[] } ) => { - const engineId: string | undefined = state.engineIds[0]; // TODO: 複数エンジン対応 + const engineId: string | undefined = state.engineIds[0]; // TODO: 複数エンジン対応, 暫定的に音声結合機能は0番目のエンジンのみを使用する if (engineId === undefined) throw new Error(`No such engine registered: index == 0`); @@ -1066,9 +1108,9 @@ export const audioStore: VoiceVoxStoreOptions< }, GENERATE_AUDIO_FROM_AUDIO_ITEM: createUILockAction( async ({ dispatch, state }, { audioItem }: { audioItem: AudioItem }) => { - const engineId: string | undefined = state.engineIds[0]; // TODO: 複数エンジン対応 + const engineId = audioItem.engineId; if (engineId === undefined) - throw new Error(`No such engineId registered: index == 0`); + throw new Error(`engineId is not defined for audioItem`); const [id, audioQuery] = await generateUniqueIdAndQuery( state, @@ -1718,8 +1760,14 @@ export const audioCommandStore: VoiceVoxStoreOptions< { state, commit, dispatch }, { audioKey, text }: { audioKey: string; text: string } ) { + const engineId = state.audioItems[audioKey].engineId; + if (engineId === undefined) + throw new Error("assert engineId !== undefined"); + const styleId = state.audioItems[audioKey].styleId; - if (styleId == undefined) throw new Error("styleId != undefined"); + if (styleId === undefined) + throw new Error("assert styleId !== undefined"); + const query: AudioQuery | undefined = state.audioItems[audioKey].query; try { if (query !== undefined) { @@ -1727,6 +1775,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< "FETCH_ACCENT_PHRASES", { text, + engineId, styleId, } ); @@ -1739,6 +1788,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< } else { const newAudioQuery = await dispatch("FETCH_AUDIO_QUERY", { text, + engineId, styleId, }); commit("COMMAND_CHANGE_AUDIO_TEXT", { @@ -1759,14 +1809,15 @@ export const audioCommandStore: VoiceVoxStoreOptions< }, async COMMAND_CHANGE_STYLE_ID( { state, dispatch, commit }, - { audioKey, styleId }: { audioKey: string; styleId: number } + { + audioKey, + engineId, + styleId, + }: { audioKey: string; engineId: string; styleId: number } ) { const query = state.audioItems[audioKey].query; try { - await dispatch("SETUP_SPEAKER", { - audioKey, - styleId, - }); + await dispatch("SETUP_SPEAKER", { audioKey, engineId, styleId }); if (query !== undefined) { const accentPhrases = query.accentPhrases; @@ -1774,12 +1825,14 @@ export const audioCommandStore: VoiceVoxStoreOptions< "FETCH_MORA_DATA", { accentPhrases, + engineId, styleId, } ); commit("COMMAND_CHANGE_STYLE_ID", { + engineId, styleId, - audioKey: audioKey, + audioKey, update: "AccentPhrases", accentPhrases: newAccentPhrases, }); @@ -1787,9 +1840,11 @@ export const audioCommandStore: VoiceVoxStoreOptions< const text = state.audioItems[audioKey].text; const query: AudioQuery = await dispatch("FETCH_AUDIO_QUERY", { text: text, + engineId, styleId, }); commit("COMMAND_CHANGE_STYLE_ID", { + engineId, styleId, audioKey, update: "AudioQuery", @@ -1798,6 +1853,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< } } catch (error) { commit("COMMAND_CHANGE_STYLE_ID", { + engineId, styleId, audioKey, update: "StyleId", @@ -1825,12 +1881,19 @@ export const audioCommandStore: VoiceVoxStoreOptions< newAccentPhrases[accentPhraseIndex].accent = accent; try { + const engineId = state.audioItems[audioKey].engineId; + if (engineId === undefined) + throw new Error("assert engineId !== undefined"); + const styleId = state.audioItems[audioKey].styleId; - if (styleId == undefined) throw new Error("styleId != undefined"); + if (styleId === undefined) + throw new Error("assert styleId !== undefined"); + const resultAccentPhrases: AccentPhrase[] = await dispatch( "FETCH_AND_COPY_MORA_DATA", { accentPhrases: newAccentPhrases, + engineId, styleId, copyIndexes: [accentPhraseIndex], } @@ -1866,8 +1929,15 @@ export const audioCommandStore: VoiceVoxStoreOptions< ) { const { audioKey, accentPhraseIndex } = payload; const query: AudioQuery | undefined = state.audioItems[audioKey].query; + + const engineId = state.audioItems[audioKey].engineId; + if (engineId === undefined) + throw new Error("assert engineId !== undefined"); + const styleId = state.audioItems[audioKey].styleId; - if (styleId == undefined) throw new Error("styleId != undefined"); + if (styleId === undefined) + throw new Error("assert styleId !== undefined"); + if (query === undefined) { throw Error( "`COMMAND_CHANGE_ACCENT_PHRASE_SPLIT` should not be called if the query does not exist." @@ -1946,6 +2016,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< "FETCH_AND_COPY_MORA_DATA", { accentPhrases: newAccentPhrases, + engineId, styleId, copyIndexes: changeIndexes, } @@ -1976,8 +2047,13 @@ export const audioCommandStore: VoiceVoxStoreOptions< popUntilPause: boolean; } ) { + const engineId = state.audioItems[audioKey].engineId; + if (engineId === undefined) + throw new Error("assert engineId !== undefined"); + const styleId = state.audioItems[audioKey].styleId; - if (styleId == undefined) throw new Error("styleId != undefined"); + if (styleId === undefined) + throw new Error("assert styleId !== undefined"); let newAccentPhrasesSegment: AccentPhrase[] | undefined = undefined; @@ -1999,6 +2075,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< // 判別できない読み仮名が混じっていた場合400エラーが帰るのでfallback newAccentPhrasesSegment = await dispatch("FETCH_ACCENT_PHRASES", { text: pureKatakanaWithAccent, + engineId, styleId, isKana: true, }).catch( @@ -2006,6 +2083,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< () => dispatch("FETCH_ACCENT_PHRASES", { text: newPronunciation, + engineId, styleId, isKana: false, }) @@ -2013,6 +2091,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< } else { newAccentPhrasesSegment = await dispatch("FETCH_ACCENT_PHRASES", { text: newPronunciation, + engineId, styleId, }); } @@ -2048,6 +2127,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< "FETCH_AND_COPY_MORA_DATA", { accentPhrases: newAccentPhrases, + engineId, styleId, copyIndexes, } @@ -2067,14 +2147,20 @@ export const audioCommandStore: VoiceVoxStoreOptions< { state, dispatch, commit }, { audioKey } ) { + const engineId = state.audioItems[audioKey].engineId; + if (engineId === undefined) + throw new Error("assert engineId !== undefined"); + const styleId = state.audioItems[audioKey].styleId; - if (styleId == undefined) throw new Error("styleId == undefined"); + if (styleId === undefined) + throw new Error("assert styleId !== undefined"); const query = state.audioItems[audioKey].query; - if (query == undefined) throw new Error("query == undefined"); + if (query === undefined) throw new Error("assert query !== undefined"); const newAccentPhases = await dispatch("FETCH_MORA_DATA", { accentPhrases: query.accentPhrases, + engineId, styleId, }); @@ -2087,6 +2173,9 @@ export const audioCommandStore: VoiceVoxStoreOptions< { state, dispatch, commit }, { audioKey, accentPhraseIndex } ) { + const engineId = state.audioItems[audioKey].engineId; + if (engineId == undefined) throw new Error("engineId == undefined"); + const styleId = state.audioItems[audioKey].styleId; if (styleId == undefined) throw new Error("styleId == undefined"); @@ -2095,6 +2184,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< const newAccentPhases = await dispatch("FETCH_AND_COPY_MORA_DATA", { accentPhrases: [...query.accentPhrases], + engineId, styleId, copyIndexes: [accentPhraseIndex], }); @@ -2214,7 +2304,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< if (!getters.USER_ORDERED_CHARACTER_INFOS) throw new Error("USER_ORDERED_CHARACTER_INFOS == undefined"); - for (const { text, styleId } of parseTextFile( + for (const { text, engineId, styleId } of parseTextFile( body, state.defaultStyleIds, getters.USER_ORDERED_CHARACTER_INFOS @@ -2224,6 +2314,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< audioItems.push( await dispatch("GENERATE_AUDIO_ITEM", { text, + engineId, styleId, baseAudioItem, }) @@ -2248,10 +2339,12 @@ export const audioCommandStore: VoiceVoxStoreOptions< { prevAudioKey, texts, + engineId, styleId, }: { prevAudioKey: string; texts: string[]; + engineId: string; styleId: number; } ) => { @@ -2269,6 +2362,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< //パラメータ引き継ぎがOFFの場合、baseAudioItemがundefinedになっているのでパラメータ引き継ぎは行われない const audioItem = await dispatch("GENERATE_AUDIO_ITEM", { text, + engineId, styleId, baseAudioItem, presetKey: basePresetKey, @@ -2345,7 +2439,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< }, COMMAND_CHANGE_STYLE_ID( draft, - payload: { styleId: number; audioKey: string } & ( + payload: { engineId: string; styleId: number; audioKey: string } & ( | { update: "StyleId"; } @@ -2361,6 +2455,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< ) { audioStore.mutations.SET_AUDIO_STYLE_ID(draft, { audioKey: payload.audioKey, + engineId: payload.engineId, styleId: payload.styleId, }); if (payload.update == "AccentPhrases") { diff --git a/src/store/dictionary.ts b/src/store/dictionary.ts index 9cacfe5d56..a3e03e7031 100644 --- a/src/store/dictionary.ts +++ b/src/store/dictionary.ts @@ -17,10 +17,7 @@ export const dictionaryStore: VoiceVoxStoreOptions< getters: {}, mutations: {}, actions: { - LOAD_USER_DICT: async ({ state, dispatch }) => { - const engineId: string | undefined = state.engineIds[0]; // TODO: 複数エンジン対応 - if (engineId === undefined) - throw new Error(`No such engine registered: index == 0`); + LOAD_USER_DICT: async ({ dispatch }, { engineId }) => { const engineDict = await dispatch("INSTANTIATE_ENGINE_CONNECTOR", { engineId, }).then((instance) => instance.invoke("getUserDictWordsUserDictGet")({})); diff --git a/src/store/index.ts b/src/store/index.ts index 54812bcf8a..1f2ddf4024 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -19,6 +19,7 @@ import { audioStore, audioCommandStore, audioCommandStoreState, + getCharacterInfo, } from "./audio"; import { projectStoreState, projectStore } from "./project"; import { uiStoreState, uiStore } from "./ui"; @@ -44,7 +45,18 @@ export const indexStore: VoiceVoxStoreOptions< IndexActions, IndexMutations > = { - getters: {}, + getters: { + /** + * すべてのエンジンのキャラクター情報のリスト。 + * キャラクター情報が読み出されていないときは、空リストを返す。 + */ + GET_FLATTEN_CHARACTER_INFOS(state) { + const flattenCharacterInfos = state.engineIds.flatMap( + (engineId) => state.characterInfos[engineId] ?? [] + ); + return flattenCharacterInfos; + }, + }, mutations: { SET_DEFAULT_STYLE_IDS(state, { defaultStyleIds }) { state.defaultStyleIds = defaultStyleIds; @@ -54,14 +66,20 @@ export const indexStore: VoiceVoxStoreOptions< if (state.audioKeys.length === 1) { const audioItem = state.audioItems[state.audioKeys[0]]; if (audioItem.text === "") { - const characterInfo = state.characterInfos?.find( - (info) => - info.metas.styles.find( - (style) => style.styleId == audioItem.styleId - ) != undefined + if (audioItem.engineId === undefined) + throw new Error("assert audioItem.engineId !== undefined"); + + if (audioItem.styleId === undefined) + throw new Error("assert audioItem.styleId !== undefined"); + + const characterInfo = getCharacterInfo( + state, + audioItem.engineId, + audioItem.styleId ); - if (characterInfo == undefined) - throw new Error("characterInfo == undefined"); + + if (characterInfo === undefined) + throw new Error("assert characterInfo !== undefined"); const speakerUuid = characterInfo.metas.speakerUuid; const defaultStyleId = defaultStyleIds.find( @@ -120,11 +138,11 @@ export const indexStore: VoiceVoxStoreOptions< userCharacterOrder ); }, - GET_NEW_CHARACTERS({ state }) { - if (!state.characterInfos) throw new Error("characterInfos is undefined"); + GET_NEW_CHARACTERS({ state, getters }) { + const flattenCharacterInfos = getters.GET_FLATTEN_CHARACTER_INFOS; // キャラクター表示順序に含まれていなければ新規キャラとみなす - const allSpeakerUuid = state.characterInfos.map( + const allSpeakerUuid = flattenCharacterInfos.map( (characterInfo) => characterInfo.metas.speakerUuid ); const newSpeakerUuid = allSpeakerUuid.filter( @@ -135,14 +153,14 @@ export const indexStore: VoiceVoxStoreOptions< async IS_UNSET_DEFAULT_STYLE_ID(_, { speakerUuid }) { return await window.electron.isUnsetDefaultStyleId(speakerUuid); }, - async LOAD_DEFAULT_STYLE_IDS({ commit, state }) { + async LOAD_DEFAULT_STYLE_IDS({ commit, getters }) { let defaultStyleIds = await window.electron.getSetting("defaultStyleIds"); - if (!state.characterInfos) throw new Error("characterInfos is undefined"); + const flattenCharacterInfos = getters.GET_FLATTEN_CHARACTER_INFOS; // デフォルトスタイルが設定されていない場合は0をセットする // FIXME: 保存しているものとstateのものが異なってしまうので良くない。デフォルトスタイルが未設定の場合はAudioCellsを表示しないようにすべき - const unsetCharacterInfos = state.characterInfos.filter( + const unsetCharacterInfos = flattenCharacterInfos.filter( (characterInfo) => !defaultStyleIds.some( (styleId) => styleId.speakerUuid == characterInfo.metas.speakerUuid diff --git a/src/store/project.ts b/src/store/project.ts index 4e92057c53..74f9dbacb6 100755 --- a/src/store/project.ts +++ b/src/store/project.ts @@ -123,6 +123,8 @@ export const projectStore: VoiceVoxStoreOptions< }; // Migration + const engineId = "074fc39e-678b-4c13-8916-ffca8d505d1d"; + if ( semver.satisfies(projectAppVersion, "<0.4", semverSatisfiesOptions) ) { @@ -170,6 +172,7 @@ export const projectStore: VoiceVoxStoreOptions< await context .dispatch("FETCH_MORA_DATA", { accentPhrases: audioItem.query.accentPhrases, + engineId, styleId: audioItem.characterIndex, }) .then((accentPhrases: AccentPhrase[]) => { @@ -223,6 +226,17 @@ export const projectStore: VoiceVoxStoreOptions< } } + if ( + semver.satisfies(projectAppVersion, "<0.14", semverSatisfiesOptions) + ) { + for (const audioItemsKey in obj.audioItems) { + const audioItem = obj.audioItems[audioItemsKey]; + if (audioItem.engineId === undefined) { + audioItem.engineId = engineId; + } + } + } + // Validation check const ajv = new Ajv(); const validate = ajv.compile(projectSchema); @@ -235,6 +249,16 @@ export const projectStore: VoiceVoxStoreOptions< " Every audioKey in audioKeys should be a key of audioItems" ); } + if ( + !obj.audioKeys.every( + (audioKey) => obj.audioItems[audioKey].engineId != undefined + ) + ) { + throw new Error( + 'Every audioItem should have a "engineId" attribute.' + ); + } + // FIXME: assert engineId is registered if ( !obj.audioKeys.every( (audioKey) => obj.audioItems[audioKey].styleId != undefined @@ -389,6 +413,7 @@ const audioItemSchema = { text: { type: "string" }, }, optionalProperties: { + engineId: { type: "string" }, styleId: { type: "int32" }, query: audioQuerySchema, presetKey: { type: "string" }, diff --git a/src/store/type.ts b/src/store/type.ts index b7cc2bbcfa..b92754eb87 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -32,8 +32,10 @@ import { import { IEngineConnectorFactory } from "@/infrastructures/EngineConnector"; import { QVueGlobals } from "quasar"; +// FIXME: SpeakerIdを追加する export type AudioItem = { text: string; + engineId?: string; styleId?: number; query?: AudioQuery; presetKey?: string; @@ -84,7 +86,7 @@ export type QuasarDialog = QVueGlobals["dialog"]; export type AudioStoreState = { engineStates: Record; - characterInfos?: CharacterInfo[]; + characterInfos: Record; audioKeyInitializingSpeaker?: string; audioItems: Record; audioKeys: string[]; @@ -146,12 +148,20 @@ type AudioStoreTypes = { mutation: { engineId: string; engineState: EngineState }; }; - LOAD_CHARACTER: { + LOAD_CHARACTER_ALL: { action(): void; }; + LOAD_CHARACTER: { + action(payload: { engineId: string }): void; + }; + SET_CHARACTER_INFOS: { - mutation: { characterInfos: CharacterInfo[] }; + mutation: { engineId: string; characterInfos: CharacterInfo[] }; + }; + + CHARACTER_INFO: { + getter(engineId: string, styleId: number): CharacterInfo | undefined; }; USER_ORDERED_CHARACTER_INFOS: { @@ -163,15 +173,19 @@ type AudioStoreTypes = { }; IS_INITIALIZED_ENGINE_SPEAKER: { - action(payload: { styleId: number }): Promise; + action(payload: { engineId: string; styleId: number }): Promise; }; INITIALIZE_ENGINE_SPEAKER: { - action(payload: { styleId: number }): void; + action(payload: { engineId: string; styleId: number }): void; }; SETUP_SPEAKER: { - action(payload: { audioKey: string; styleId: number }): void; + action(payload: { + audioKey: string; + engineId: string; + styleId: number; + }): void; }; SET_AUDIO_KEY_INITIALIZING_SPEAKER: { @@ -203,6 +217,7 @@ type AudioStoreTypes = { GENERATE_AUDIO_ITEM: { action(payload: { text?: string; + engineId?: string; styleId?: number; presetKey?: string; baseAudioItem?: AudioItem; @@ -285,11 +300,15 @@ type AudioStoreTypes = { }; FETCH_AUDIO_QUERY: { - action(payload: { text: string; styleId: number }): Promise; + action(payload: { + text: string; + engineId: string; + styleId: number; + }): Promise; }; SET_AUDIO_STYLE_ID: { - mutation: { audioKey: string; styleId: number }; + mutation: { audioKey: string; engineId: string; styleId: number }; }; SET_ACCENT_PHRASES: { @@ -299,6 +318,7 @@ type AudioStoreTypes = { FETCH_ACCENT_PHRASES: { action(payload: { text: string; + engineId: string; styleId: number; isKana?: boolean; }): Promise; @@ -329,6 +349,7 @@ type AudioStoreTypes = { FETCH_MORA_DATA: { action(payload: { accentPhrases: AccentPhrase[]; + engineId: string; styleId: number; }): Promise; }; @@ -336,6 +357,7 @@ type AudioStoreTypes = { FETCH_AND_COPY_MORA_DATA: { action(payload: { accentPhrases: AccentPhrase[]; + engineId: string; styleId: number; copyIndexes: number[]; }): Promise; @@ -475,12 +497,16 @@ type AudioCommandStoreTypes = { }; COMMAND_CHANGE_STYLE_ID: { - mutation: { styleId: number; audioKey: string } & ( + mutation: { engineId: string; styleId: number; audioKey: string } & ( | { update: "StyleId" } | { update: "AccentPhrases"; accentPhrases: AccentPhrase[] } | { update: "AudioQuery"; query: AudioQuery } ); - action(payload: { audioKey: string; styleId: number }): void; + action(payload: { + audioKey: string; + engineId: string; + styleId: number; + }): void; }; COMMAND_CHANGE_ACCENT: { @@ -617,6 +643,7 @@ type AudioCommandStoreTypes = { action(payload: { prevAudioKey: string; texts: string[]; + engineId: string; styleId: number; }): string[]; }; @@ -680,6 +707,10 @@ export type IndexStoreState = { }; type IndexStoreTypes = { + GET_FLATTEN_CHARACTER_INFOS: { + getter: CharacterInfo[]; + }; + GET_HOW_TO_USE_TEXT: { action(): Promise; }; @@ -1115,7 +1146,9 @@ export type DictionaryStoreState = Record; type DictionaryStoreTypes = { LOAD_USER_DICT: { - action(): Promise>; + action(payload: { + engineId: string; + }): Promise>; }; ADD_WORD: { action(payload: { diff --git a/src/type/preload.ts b/src/type/preload.ts index e575c608cc..c2bfcf579e 100644 --- a/src/type/preload.ts +++ b/src/type/preload.ts @@ -157,6 +157,7 @@ export type SavingSetting = { audioOutputDevice: string; }; +// FIXME: engineIdを追加 export type DefaultStyleId = { speakerUuid: string; defaultStyleId: number; diff --git a/src/views/Home.vue b/src/views/Home.vue index 375f23256e..4b09148f41 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -132,13 +132,13 @@ @@ -368,9 +368,11 @@ export default defineComponent({ ); const addAudioItem = async () => { const prevAudioKey = activeAudioKey.value; + let engineId: string | undefined = undefined; let styleId: number | undefined = undefined; let presetKey: string | undefined = undefined; if (prevAudioKey !== undefined) { + engineId = store.state.audioItems[prevAudioKey].engineId; styleId = store.state.audioItems[prevAudioKey].styleId; presetKey = store.state.audioItems[prevAudioKey].presetKey; } @@ -384,6 +386,7 @@ export default defineComponent({ //パラメータ引き継ぎがONの場合は話速等のパラメータを引き継いでテキスト欄を作成する //パラメータ引き継ぎがOFFの場合、baseAudioItemがundefinedになっているのでパラメータ引き継ぎは行われない audioItem = await store.dispatch("GENERATE_AUDIO_ITEM", { + engineId, styleId, presetKey, baseAudioItem, @@ -468,7 +471,7 @@ export default defineComponent({ await store.dispatch("GET_ENGINE_INFOS"); await store.dispatch("START_WAITING_ENGINE_ALL"); - await store.dispatch("LOAD_CHARACTER"); + await store.dispatch("LOAD_CHARACTER_ALL"); await store.dispatch("LOAD_USER_CHARACTER_ORDER"); await store.dispatch("LOAD_DEFAULT_STYLE_IDS"); @@ -478,8 +481,7 @@ export default defineComponent({ // スタイルが複数あって未選択なキャラがいる場合はデフォルトスタイル選択ダイアログを表示 let isUnsetDefaultStyleIds = false; - if (characterInfos.value == undefined) throw new Error(); - for (const info of characterInfos.value) { + for (const info of flattenCharacterInfos.value) { isUnsetDefaultStyleIds ||= info.metas.styles.length > 1 && (await store.dispatch("IS_UNSET_DEFAULT_STYLE_ID", { @@ -499,9 +501,10 @@ export default defineComponent({ focusCell({ audioKey: newAudioKey }); // 最初の話者を初期化 - if (audioItem.styleId != undefined) { + if (audioItem.engineId != undefined && audioItem.styleId != undefined) { store.dispatch("SETUP_SPEAKER", { audioKey: newAudioKey, + engineId: audioItem.engineId, styleId: audioItem.styleId, }); } @@ -589,7 +592,9 @@ export default defineComponent({ }); // キャラクター並び替え - const characterInfos = computed(() => store.state.characterInfos); + const flattenCharacterInfos = computed( + () => store.getters.GET_FLATTEN_CHARACTER_INFOS + ); const isCharacterOrderDialogOpenComputed = computed({ get: () => !store.state.isAcceptTermsDialogOpen && @@ -690,7 +695,7 @@ export default defineComponent({ isSettingDialogOpenComputed, isHotkeySettingDialogOpenComputed, isToolbarSettingDialogOpenComputed, - characterInfos, + flattenCharacterInfos, isCharacterOrderDialogOpenComputed, isDefaultStyleSelectDialogOpenComputed, isDictionaryManageDialogOpenComputed, diff --git a/tests/unit/store/Vuex.spec.ts b/tests/unit/store/Vuex.spec.ts index e2ce354206..d0be83c6ff 100644 --- a/tests/unit/store/Vuex.spec.ts +++ b/tests/unit/store/Vuex.spec.ts @@ -21,6 +21,7 @@ describe("store/vuex.js test", () => { engineStates: { "88022f86-c823-436e-85a3-500c629749c4": "STARTING", }, + characterInfos: {}, defaultStyleIds: [], userCharacterOrder: [], audioItems: {}, @@ -139,6 +140,7 @@ describe("store/vuex.js test", () => { store.state.engineIds.forEach((engineId) => assert.equal(store.state.engineStates[engineId], "STARTING") ); + assert.isObject(store.state.characterInfos); assert.isArray(store.state.defaultStyleIds); assert.isObject(store.state.audioItems); assert.isEmpty(store.state.audioItems);