diff --git a/src/components/AudioCell.vue b/src/components/AudioCell.vue index 9cc44609b9..ada7888148 100644 --- a/src/components/AudioCell.vue +++ b/src/components/AudioCell.vue @@ -34,6 +34,7 @@ " @click=" changeStyleId( + characterInfo.metas.speakerUuid, getDefaultStyle(characterInfo.metas.speakerUuid).styleId ) " @@ -89,7 +90,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 ); @@ -245,9 +252,22 @@ export default defineComponent({ } }; - const changeStyleId = (styleId: number) => { + const changeStyleId = (speakerUuid: string, styleId: number) => { + const engineKey = store.state.engineKeys.find((_engineKey) => + (store.state.characterInfos[_engineKey] ?? []).some( + (characterInfo) => characterInfo.metas.speakerUuid === speakerUuid + ) + ); + if (engineKey === undefined) + throw new Error( + `No engineKey for target character style (speakerUuid == ${speakerUuid}, styleId == ${styleId})` + ); + + const engineId = getEngineIdByEngineKey(store.state, engineKey); + store.dispatch("COMMAND_CHANGE_STYLE_ID", { audioKey: props.audioKey, + engineId, styleId, }); }; @@ -305,10 +325,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 3ba0c15e08..d6e422b857 100644 --- a/src/components/CharacterPortrait.vue +++ b/src/components/CharacterPortrait.vue @@ -16,18 +16,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 20611e126c..4e17d61cbc 100644 --- a/src/components/DictionaryManageDialog.vue +++ b/src/components/DictionaryManageDialog.vue @@ -222,6 +222,7 @@ import { import AudioAccent from "@/components/AudioAccent.vue"; import { QInput, useQuasar } from "quasar"; import { AudioItem } from "@/store/type"; +import { getEngineIdByEngineKey } from "@/store/audio"; export default defineComponent({ name: "DictionaryManageDialog", @@ -237,6 +238,8 @@ export default defineComponent({ const store = useStore(); const $q = useQuasar(); + const engineKeyComputed = computed(() => store.state.engineKeys[0]); // TODO: 複数エンジン対応 + const dictionaryManageDialogOpenedComputed = computed({ get: () => props.modelValue, set: (val) => emit("update:modelValue", val), @@ -256,10 +259,16 @@ export default defineComponent({ }; const loadingDictProcess = async () => { + const engineKey = engineKeyComputed.value; + if (engineKey === undefined) + throw new Error(`assert engineKey !== undefined`); + loadingDict.value = true; try { userDict.value = await createUILockAction( - store.dispatch("LOAD_USER_DICT") + store.dispatch("LOAD_USER_DICT", { + engineKey, + }) ); } catch { $q.dialog({ @@ -330,6 +339,10 @@ export default defineComponent({ surface.value = convertHankakuToZenkaku(text); }; const setYomi = async (text: string, changeWord?: boolean) => { + const engineKey = engineKeyComputed.value; + if (engineKey === undefined) + throw new Error(`assert engineKey !== undefined`); + // テキスト長が0の時にエラー表示にならないように、テキスト長を考慮する isOnlyHiraOrKana.value = !text.length || kanaRegex.test(text); // 読みが変更されていない場合は、アクセントフレーズに変更を加えない @@ -352,6 +365,7 @@ export default defineComponent({ await createUILockAction( store.dispatch("FETCH_ACCENT_PHRASES", { text: text + "ガ'", + engineKey, styleId: styleId.value, isKana: true, }) @@ -370,12 +384,17 @@ export default defineComponent({ }; const changeAccent = async (_: number, accent: number) => { + const engineKey = engineKeyComputed.value; + if (engineKey === undefined) + throw new Error(`assert engineKey !== undefined`); + if (accentPhrase.value) { accentPhrase.value.accent = accent; accentPhrase.value = ( await createUILockAction( store.dispatch("FETCH_MORA_DATA", { accentPhrases: [accentPhrase.value], + engineKey, styleId: styleId.value, }) ) @@ -387,6 +406,12 @@ export default defineComponent({ audioElem.pause(); const play = async () => { + const engineKey = engineKeyComputed.value; + if (engineKey === undefined) + throw new Error(`assert engineKey !== undefined`); + + const engineId = getEngineIdByEngineKey(store.state, engineKey); + if (!accentPhrase.value) return; nowGenerating.value = true; const query: AudioQuery = { @@ -403,6 +428,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..fa91271f48 100644 --- a/src/components/LibraryPolicy.vue +++ b/src/components/LibraryPolicy.vue @@ -39,13 +39,16 @@ import { useStore } from "@/store"; import { computed, defineComponent, ref } from "@vue/runtime-core"; import { useMarkdownIt } from "@/plugins/markdownItPlugin"; +import { getFlattenCharacterInfos } from "@/store/audio"; export default defineComponent({ setup() { const store = useStore(); const md = useMarkdownIt(); - const characterInfos = computed(() => store.state.characterInfos); + const flattenCharacterInfos = computed(() => + getFlattenCharacterInfos(store.state) + ); const convertMarkdown = (text: string) => { return md.render(text); @@ -62,7 +65,7 @@ export default defineComponent({ }; return { - characterInfos, + characterInfos: flattenCharacterInfos, convertMarkdown, selectCharacterInfIndex, detailIndex, diff --git a/src/store/audio.ts b/src/store/audio.ts index 9cc2cc8dae..ecbf7388b2 100644 --- a/src/store/audio.ts +++ b/src/store/audio.ts @@ -53,6 +53,7 @@ async function generateUniqueIdAndQuery( JSON.stringify([ audioItem.text, audioQuery, + audioItem.engineId, audioItem.styleId, state.experimentalSetting.enableInterrogativeUpspeak, // このフラグが違うと、同じAudioQueryで違う音声が生成されるので追加 ]) @@ -119,23 +120,28 @@ function buildFileName(state: State, audioKey: string) { 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; - } + const character = getCharacterInfo( + state, + audioItem.engineId, + audioItem.styleId + ); + if (character === undefined) + throw new Error("assert character !== undefined"); - return result > -1; - }); + const style = character.metas.styles.find( + (style) => style.styleId === audioItem.styleId + ); + if (style === undefined) throw new Error("assert style !== undefined"); - if (character === undefined) { - throw new Error(); - } + const styleName: string | undefined = style.styleName; + if (styleName === undefined) + throw new Error("assert styleName !== undefined"); return buildFileNameFromRawData(fileNamePattern, { characterName: character.metas.speakerName, @@ -162,11 +168,58 @@ function generateWriteErrorMessage(writeFileErrorResult: WriteFileErrorResult) { return `何らかの理由で失敗しました。${writeFileErrorResult.message}`; } +// FIXME: 暫定的にengineKey == engineIdとして使う +export function getEngineIdByEngineKey( + state: State, + engineKey: string +): string { + return engineKey; +} + +// FIXME: 暫定的にengineKey == engineIdとして使う +// TODO: ブランドを表すengineIdから設定項目を表すengineKeyを引くロジック +export function getDefaultEngineKeyByEngineId( + state: State, + engineId: string +): string { + return engineId; +} + +export function getFlattenCharacterInfos(state: State): CharacterInfo[] { + const flattenCharacterInfos = state.engineKeys.flatMap( + (engineKey) => state.characterInfos[engineKey] ?? [] + ); + + // まだキャラクター情報が読み出されていないときは、例外を投げる + // if (flattenCharacterInfos.length === 0) + // throw new Error("CharacterInfos not fetched yet"); + + // まだキャラクター情報が読み出されていないときは、空リストを返す + return flattenCharacterInfos; +} + +export function getCharacterInfo( + state: State, + engineId: string, + styleId: number +): CharacterInfo | undefined { + const engineKey = getDefaultEngineKeyByEngineId(state, engineId); + const engineCharacterInfos = state.characterInfos[engineKey]; + + // (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: {}, @@ -221,13 +274,26 @@ export const audioStore: VoiceVoxStoreOptions< ? audioElements[state._activeAudioKey]?.currentTime : undefined; }, + CHARACTER_INFO: (state) => (engineId, styleId) => { + return getCharacterInfo(state, engineId, styleId); + }, 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) - ); + let characterInfoList: CharacterInfo[] = []; + for (const engineKey of state.engineKeys) { + const engineCharacterInfos: CharacterInfo[] | undefined = + state.characterInfos[engineKey]; + if (engineCharacterInfos === undefined) continue; + + characterInfoList = characterInfoList.concat(engineCharacterInfos); + } + + return characterInfoList.length !== 0 + ? characterInfoList.sort( + (a, b) => + state.userCharacterOrder.indexOf(a.metas.speakerUuid) - + state.userCharacterOrder.indexOf(b.metas.speakerUuid) + ) + : undefined; }, }, @@ -243,9 +309,16 @@ export const audioStore: VoiceVoxStoreOptions< }, SET_CHARACTER_INFOS( state, - { characterInfos }: { characterInfos: CharacterInfo[] } + { + engineKey, + characterInfos, + }: { engineKey: string; characterInfos: CharacterInfo[] } ) { - state.characterInfos = characterInfos; + const allCharacterInfos: Record = JSON.parse( + JSON.stringify(state.characterInfos) + ); + allCharacterInfos[engineKey] = characterInfos; + state.characterInfos = allCharacterInfos; }, SET_ACTIVE_AUDIO_KEY(state, { audioKey }: { audioKey?: string }) { state._activeAudioKey = audioKey; @@ -432,8 +505,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( @@ -574,80 +652,87 @@ export const audioStore: VoiceVoxStoreOptions< } } ), - LOAD_CHARACTER: createUILockAction(async ({ state, commit, dispatch }) => { - const engineKey: string | undefined = state.engineKeys[0]; // TODO: 複数エンジン対応 - if (engineKey === undefined) - throw new Error(`No such engine registered: index == 0`); - - const speakers = await dispatch("INVOKE_ENGINE_CONNECTOR", { - engineKey, - action: "speakersSpeakersGet", - // 連想配列が第一引数になければ失敗する - payload: [{}], - }) - .then(toDispatchResponse("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("INVOKE_ENGINE_CONNECTOR", { + LOAD_CHARACTER_ALL: createUILockAction(async ({ state, dispatch }) => { + for (const engineKey of state.engineKeys) { + window.electron.logInfo(`Load CharacterInfo from engine ${engineKey}`); + await dispatch("LOAD_CHARACTER", { engineKey }); + } + }), + LOAD_CHARACTER: createUILockAction( + async ({ commit, dispatch }, { engineKey }) => { + const speakers = await dispatch("INVOKE_ENGINE_CONNECTOR", { engineKey, - action: "speakerInfoSpeakerInfoGet", - payload: [{ speakerUuid: speaker.speakerUuid }], + action: "speakersSpeakersGet", + // 連想配列が第一引数になければ失敗する + payload: [{}], }) - .then(toDispatchResponse("speakerInfoSpeakerInfoGet")) + .then(toDispatchResponse("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("INVOKE_ENGINE_CONNECTOR", { + engineKey, + action: "speakerInfoSpeakerInfoGet", + payload: [{ speakerUuid: speaker.speakerUuid }], + }) + .then(toDispatchResponse("speakerInfoSpeakerInfoGet")) + .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", { engineKey, characterInfos }); + } + ), GENERATE_AUDIO_KEY() { const audioKey = uuidv4(); audioElements[audioKey] = new Audio(); @@ -657,15 +742,11 @@ export const audioStore: VoiceVoxStoreOptions< * 指定した話者(スタイルID)に対してエンジン側の初期化を行い、即座に音声合成ができるようにする。 * 初期化済みだった場合は何もしない。 */ - async SETUP_ENGINE_SPEAKER({ state, dispatch }, { styleId }) { - const engineInfo = state.engineInfos[0]; // TODO: 複数エンジン対応 - if (!engineInfo) - throw new Error(`No such engineInfo registered: index == 0`); - + async SETUP_ENGINE_SPEAKER({ dispatch }, { engineKey, styleId }) { // FIXME: なぜかbooleanではなくstringが返ってくる。 // おそらくエンジン側のresponse_modelをBaseModel継承にしないといけない。 const isInitialized: string = await dispatch("INVOKE_ENGINE_CONNECTOR", { - engineKey: engineInfo.key, + engineKey, action: "isInitializedSpeakerIsInitializedSpeakerGet", payload: [{ speaker: styleId }], }); @@ -676,7 +757,7 @@ export const audioStore: VoiceVoxStoreOptions< await dispatch("ASYNC_UI_LOCK", { callback: () => dispatch("INVOKE_ENGINE_CONNECTOR", { - engineKey: engineInfo.key, + engineKey, action: "initializeSpeakerInitializeSpeakerPost", payload: [{ speaker: styleId }], }), @@ -692,6 +773,7 @@ export const audioStore: VoiceVoxStoreOptions< { state, getters, dispatch }, payload: { text?: string; + engineId?: string; styleId?: number; presetKey?: string; baseAudioItem?: AudioItem; @@ -708,29 +790,73 @@ export const audioStore: VoiceVoxStoreOptions< const text = payload.text ?? ""; - const engineKey: string | undefined = state.engineKeys[0]; // TODO: 複数エンジン対応, 暫定的に0番目のエンジンのみを使用する。将来的にGENERATE_AUDIO_ITEMの引数にengineId/engineKeyを追加する予定 - if (engineKey === undefined) - throw new Error(`No such engine registered: index == 0`); - - const styleId = - payload.styleId ?? - state.defaultStyleIds[ - state.defaultStyleIds.findIndex( - (x) => - x.speakerUuid === userOrderedCharacterInfos[0].metas.speakerUuid // FIXME: defaultStyleIds内にspeakerUuidがない場合がある + let engineId: string | undefined = undefined; + let engineKey: string | undefined = undefined; + let styleId: number | undefined = undefined; + + if ( + (payload.engineId !== undefined && payload.styleId === undefined) || + (payload.engineId === undefined && payload.styleId !== undefined) + ) + throw new Error( + "(engineId, styleId) must be (defined, defined) or (undefined, undefined)" + ); + + if (payload.engineId !== undefined && payload.styleId !== undefined) { + engineId = payload.engineId; + engineKey = getDefaultEngineKeyByEngineId(state, engineId); + styleId = payload.styleId; + } else { + // select default style if (engineId, styleId) === (undefined, undefined) + const defaultCharacterInfo: CharacterInfo | undefined = + userOrderedCharacterInfos[0]; + if (defaultCharacterInfo === undefined) + throw new Error(`No default characterInfo (no character loaded)`); + + const defaultStyleId = state.defaultStyleIds.find( + (defaultStyleId) => + defaultStyleId.speakerUuid === + defaultCharacterInfo.metas.speakerUuid + ); // FIXME: defaultStyleIds内にspeakerUuidがない場合がある + if (defaultStyleId === undefined) throw new Error(`No defaultStyleId`); + + // 最初に一致するspeakerUuidをもつengineInfoを使用する + // FIXME: 同一のengineIdを持つ複数のengineKeyが存在する場合の処理 + const firstMatchEngineKey = state.engineKeys.find((engineKey) => + (state.characterInfos[engineKey] ?? []).some( + (characterInfo) => + characterInfo.metas.speakerUuid === defaultStyleId.speakerUuid ) - ].defaultStyleId; + ); + if (firstMatchEngineKey === undefined) + throw new Error(`No engineKey for defaultStyleId`); + + engineKey = firstMatchEngineKey; + engineId = getEngineIdByEngineKey(state, engineKey); + styleId = defaultStyleId.defaultStyleId; + } + + if (engineKey === undefined) + throw new Error(`assert engineKey != undefined`); + + if (engineId === undefined) + throw new Error(`assert engineId != undefined`); + + if (styleId === undefined) throw new Error(`assert styleId != undefined`); + const baseAudioItem = payload.baseAudioItem; const query = getters.IS_ENGINE_READY(engineKey) ? await dispatch("FETCH_AUDIO_QUERY", { text, + engineKey, styleId, }).catch(() => undefined) : undefined; const audioItem: AudioItem = { text, + engineId, styleId, }; if (query != undefined) { @@ -806,21 +932,19 @@ export const audioStore: VoiceVoxStoreOptions< commit("SET_AUDIO_QUERY", payload); }, FETCH_ACCENT_PHRASES( - { state, dispatch }, + { dispatch }, { text, + engineKey, styleId, isKana, }: { text: string; + engineKey: string; styleId: number; isKana?: boolean; } ) { - const engineKey: string | undefined = state.engineKeys[0]; // TODO: 複数エンジン対応 - if (engineKey === undefined) - throw new Error(`No such engine registered: index == 0`); - return dispatch("INVOKE_ENGINE_CONNECTOR", { engineKey, action: "accentPhrasesAccentPhrasesPost", @@ -842,16 +966,13 @@ export const audioStore: VoiceVoxStoreOptions< }); }, FETCH_MORA_DATA( - { dispatch, state }, + { dispatch }, { accentPhrases, + engineKey, styleId, - }: { accentPhrases: AccentPhrase[]; styleId: number } + }: { accentPhrases: AccentPhrase[]; engineKey: string; styleId: number } ) { - const engineKey: string | undefined = state.engineKeys[0]; // TODO: 複数エンジン対応 - if (engineKey === undefined) - throw new Error(`No such engine registered: index == 0`); - return dispatch("INVOKE_ENGINE_CONNECTOR", { engineKey, action: "moraDataMoraDataPost", @@ -872,10 +993,12 @@ export const audioStore: VoiceVoxStoreOptions< { dispatch }, { accentPhrases, + engineKey, styleId, copyIndexes, }: { accentPhrases: AccentPhrase[]; + engineKey: string; styleId: number; copyIndexes: number[]; } @@ -884,6 +1007,7 @@ export const audioStore: VoiceVoxStoreOptions< "FETCH_MORA_DATA", { accentPhrases, + engineKey, styleId, } ); @@ -893,13 +1017,13 @@ export const audioStore: VoiceVoxStoreOptions< return accentPhrases; }, FETCH_AUDIO_QUERY( - { dispatch, state }, - { text, styleId }: { text: string; styleId: number } + { dispatch }, + { + text, + engineKey, + styleId, + }: { text: string; engineKey: string; styleId: number } ) { - const engineKey: string | undefined = state.engineKeys[0]; // TODO: 複数エンジン対応 - if (engineKey === undefined) - throw new Error(`No such engine registered: index == 0`); - return dispatch("INVOKE_ENGINE_CONNECTOR", { engineKey, action: "audioQueryAudioQueryPost", @@ -1008,7 +1132,7 @@ export const audioStore: VoiceVoxStoreOptions< { dispatch, state }, { encodedBlobs }: { encodedBlobs: string[] } ) => { - const engineKey: string | undefined = state.engineKeys[0]; // TODO: 複数エンジン対応 + const engineKey: string | undefined = state.engineKeys[0]; // TODO: 複数エンジン対応, 暫定的に音声結合機能は0番目のエンジンのみを使用する if (engineKey === undefined) throw new Error(`No such engine registered: index == 0`); @@ -1042,9 +1166,11 @@ export const audioStore: VoiceVoxStoreOptions< }, GENERATE_AUDIO_FROM_AUDIO_ITEM: createUILockAction( async ({ dispatch, state }, { audioItem }: { audioItem: AudioItem }) => { - const engineKey: string | undefined = state.engineKeys[0]; // TODO: 複数エンジン対応 - if (engineKey === undefined) - throw new Error(`No such engineKey registered: index == 0`); + const engineId = audioItem.engineId; + if (engineId === undefined) + throw new Error(`engineId is not defined for audioItem`); + + const engineKey = getDefaultEngineKeyByEngineId(state, engineId); const [id, audioQuery] = await generateUniqueIdAndQuery( state, @@ -1697,8 +1823,16 @@ 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 engineKey = getDefaultEngineKeyByEngineId(state, engineId); + 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) { @@ -1706,6 +1840,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< "FETCH_ACCENT_PHRASES", { text, + engineKey, styleId, } ); @@ -1718,6 +1853,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< } else { const newAudioQuery = await dispatch("FETCH_AUDIO_QUERY", { text, + engineKey, styleId, }); commit("COMMAND_CHANGE_AUDIO_TEXT", { @@ -1738,11 +1874,17 @@ 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 engineKey = getDefaultEngineKeyByEngineId(state, engineId); + const query = state.audioItems[audioKey].query; try { - await dispatch("SETUP_ENGINE_SPEAKER", { styleId }); + await dispatch("SETUP_ENGINE_SPEAKER", { engineKey, styleId }); if (query !== undefined) { const accentPhrases = query.accentPhrases; @@ -1750,12 +1892,14 @@ export const audioCommandStore: VoiceVoxStoreOptions< "FETCH_MORA_DATA", { accentPhrases, + engineKey, styleId, } ); commit("COMMAND_CHANGE_STYLE_ID", { + engineId, styleId, - audioKey: audioKey, + audioKey, update: "AccentPhrases", accentPhrases: newAccentPhrases, }); @@ -1763,9 +1907,11 @@ export const audioCommandStore: VoiceVoxStoreOptions< const text = state.audioItems[audioKey].text; const query: AudioQuery = await dispatch("FETCH_AUDIO_QUERY", { text: text, + engineKey, styleId, }); commit("COMMAND_CHANGE_STYLE_ID", { + engineId, styleId, audioKey, update: "AudioQuery", @@ -1774,6 +1920,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< } } catch (error) { commit("COMMAND_CHANGE_STYLE_ID", { + engineId, styleId, audioKey, update: "StyleId", @@ -1801,12 +1948,21 @@ 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 engineKey = getDefaultEngineKeyByEngineId(state, engineId); + 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, + engineKey, styleId, copyIndexes: [accentPhraseIndex], } @@ -1842,8 +1998,17 @@ 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 engineKey = getDefaultEngineKeyByEngineId(state, engineId); + 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." @@ -1922,6 +2087,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< "FETCH_AND_COPY_MORA_DATA", { accentPhrases: newAccentPhrases, + engineKey, styleId, copyIndexes: changeIndexes, } @@ -1952,8 +2118,15 @@ export const audioCommandStore: VoiceVoxStoreOptions< popUntilPause: boolean; } ) { + const engineId = state.audioItems[audioKey].engineId; + if (engineId === undefined) + throw new Error("assert engineId !== undefined"); + + const engineKey = getDefaultEngineKeyByEngineId(state, engineId); + 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; @@ -1975,6 +2148,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< // 判別できない読み仮名が混じっていた場合400エラーが帰るのでfallback newAccentPhrasesSegment = await dispatch("FETCH_ACCENT_PHRASES", { text: pureKatakanaWithAccent, + engineKey, styleId, isKana: true, }).catch( @@ -1982,6 +2156,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< () => dispatch("FETCH_ACCENT_PHRASES", { text: newPronunciation, + engineKey, styleId, isKana: false, }) @@ -1989,6 +2164,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< } else { newAccentPhrasesSegment = await dispatch("FETCH_ACCENT_PHRASES", { text: newPronunciation, + engineKey, styleId, }); } @@ -2024,6 +2200,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< "FETCH_AND_COPY_MORA_DATA", { accentPhrases: newAccentPhrases, + engineKey, styleId, copyIndexes, } @@ -2043,14 +2220,22 @@ 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 engineKey = getDefaultEngineKeyByEngineId(state, engineId); const newAccentPhases = await dispatch("FETCH_MORA_DATA", { accentPhrases: query.accentPhrases, + engineKey, styleId, }); @@ -2063,14 +2248,20 @@ 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"); const query = state.audioItems[audioKey].query; if (query == undefined) throw new Error("query == undefined"); + const engineKey = getDefaultEngineKeyByEngineId(state, engineId); + const newAccentPhases = await dispatch("FETCH_AND_COPY_MORA_DATA", { accentPhrases: [...query.accentPhrases], + engineKey, styleId, copyIndexes: [accentPhraseIndex], }); @@ -2190,7 +2381,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 @@ -2200,6 +2391,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< audioItems.push( await dispatch("GENERATE_AUDIO_ITEM", { text, + engineId, styleId, baseAudioItem, }) @@ -2224,10 +2416,12 @@ export const audioCommandStore: VoiceVoxStoreOptions< { prevAudioKey, texts, + engineId, styleId, }: { prevAudioKey: string; texts: string[]; + engineId: string; styleId: number; } ) => { @@ -2245,6 +2439,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< //パラメータ引き継ぎがOFFの場合、baseAudioItemがundefinedになっているのでパラメータ引き継ぎは行われない const audioItem = await dispatch("GENERATE_AUDIO_ITEM", { text, + engineId, styleId, baseAudioItem, presetKey: basePresetKey, @@ -2321,7 +2516,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< }, COMMAND_CHANGE_STYLE_ID( draft, - payload: { styleId: number; audioKey: string } & ( + payload: { engineId: string; styleId: number; audioKey: string } & ( | { update: "StyleId"; } @@ -2337,6 +2532,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 38e71b1f08..955d63e4c8 100644 --- a/src/store/dictionary.ts +++ b/src/store/dictionary.ts @@ -18,10 +18,7 @@ export const dictionaryStore: VoiceVoxStoreOptions< getters: {}, mutations: {}, actions: { - LOAD_USER_DICT: async ({ state, dispatch }) => { - const engineKey: string | undefined = state.engineKeys[0]; // TODO: 複数エンジン対応 - if (engineKey === undefined) - throw new Error(`No such engine registered: index == 0`); + LOAD_USER_DICT: async ({ dispatch }, { engineKey }) => { const engineDict = await dispatch("INVOKE_ENGINE_CONNECTOR", { engineKey, action: "getUserDictWordsUserDictGet", diff --git a/src/store/index.ts b/src/store/index.ts index aad8852e9d..6e11091e82 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -19,6 +19,8 @@ import { audioStore, audioCommandStore, audioCommandStoreState, + getCharacterInfo, + getFlattenCharacterInfos, } from "./audio"; import { projectStoreState, projectStore } from "./project"; import { uiStoreState, uiStore } from "./ui"; @@ -54,14 +56,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( @@ -116,10 +124,10 @@ export const indexStore: VoiceVoxStoreOptions< await window.electron.setUserCharacterOrder(userCharacterOrder); }, GET_NEW_CHARACTERS({ state }) { - if (!state.characterInfos) throw new Error("characterInfos is undefined"); + const flattenCharacterInfos = getFlattenCharacterInfos(state); // キャラクター表示順序に含まれていなければ新規キャラとみなす - const allSpeakerUuid = state.characterInfos.map( + const allSpeakerUuid = flattenCharacterInfos.map( (characterInfo) => characterInfo.metas.speakerUuid ); const newSpeakerUuid = allSpeakerUuid.filter( @@ -133,11 +141,11 @@ export const indexStore: VoiceVoxStoreOptions< async LOAD_DEFAULT_STYLE_IDS({ commit, state }) { let defaultStyleIds = await window.electron.getDefaultStyleIds(); - if (!state.characterInfos) throw new Error("characterInfos is undefined"); + const flattenCharacterInfos = getFlattenCharacterInfos(state); // デフォルトスタイルが設定されていない場合は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..b7d07c4575 100755 --- a/src/store/project.ts +++ b/src/store/project.ts @@ -170,6 +170,7 @@ export const projectStore: VoiceVoxStoreOptions< await context .dispatch("FETCH_MORA_DATA", { accentPhrases: audioItem.query.accentPhrases, + engineKey: "074fc39e-678b-4c13-8916-ffca8d505d1d", // VOICEVOX ENGINE styleId: audioItem.characterIndex, }) .then((accentPhrases: AccentPhrase[]) => { @@ -223,6 +224,17 @@ export const projectStore: VoiceVoxStoreOptions< } } + if ( + semver.satisfies(projectAppVersion, "<0.13", semverSatisfiesOptions) + ) { + for (const audioItemsKey in obj.audioItems) { + const audioItem = obj.audioItems[audioItemsKey]; + if (audioItem.engineId === undefined) { + audioItem.engineId = "074fc39e-678b-4c13-8916-ffca8d505d1d"; // VOICEVOX ENGINE + } + } + } + // Validation check const ajv = new Ajv(); const validate = ajv.compile(projectSchema); @@ -235,6 +247,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 +411,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 31d0f60add..8759e4664f 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -33,6 +33,7 @@ import { QVueGlobals } from "quasar"; export type AudioItem = { text: string; + engineId?: string; styleId?: number; query?: AudioQuery; presetKey?: string; @@ -83,7 +84,7 @@ export type QuasarDialog = QVueGlobals["dialog"]; export type AudioStoreState = { engineStates: Record; - characterInfos?: CharacterInfo[]; + characterInfos: Record; audioItems: Record; audioKeys: string[]; audioStates: Record; @@ -144,12 +145,20 @@ type AudioStoreTypes = { mutation: { engineKey: string; engineState: EngineState }; }; - LOAD_CHARACTER: { + LOAD_CHARACTER_ALL: { action(): void; }; + LOAD_CHARACTER: { + action(payload: { engineKey: string }): void; + }; + SET_CHARACTER_INFOS: { - mutation: { characterInfos: CharacterInfo[] }; + mutation: { engineKey: string; characterInfos: CharacterInfo[] }; + }; + + CHARACTER_INFO: { + getter(engineId: string, styleId: number): CharacterInfo | undefined; }; USER_ORDERED_CHARACTER_INFOS: { @@ -161,7 +170,7 @@ type AudioStoreTypes = { }; SETUP_ENGINE_SPEAKER: { - action(payload: { styleId: number }): void; + action(payload: { engineKey: string; styleId: number }): void; }; SET_ACTIVE_AUDIO_KEY: { @@ -189,6 +198,7 @@ type AudioStoreTypes = { GENERATE_AUDIO_ITEM: { action(payload: { text?: string; + engineId?: string; styleId?: number; presetKey?: string; baseAudioItem?: AudioItem; @@ -271,11 +281,15 @@ type AudioStoreTypes = { }; FETCH_AUDIO_QUERY: { - action(payload: { text: string; styleId: number }): Promise; + action(payload: { + text: string; + engineKey: string; + styleId: number; + }): Promise; }; SET_AUDIO_STYLE_ID: { - mutation: { audioKey: string; styleId: number }; + mutation: { audioKey: string; engineId: string; styleId: number }; }; SET_ACCENT_PHRASES: { @@ -285,6 +299,7 @@ type AudioStoreTypes = { FETCH_ACCENT_PHRASES: { action(payload: { text: string; + engineKey: string; styleId: number; isKana?: boolean; }): Promise; @@ -315,6 +330,7 @@ type AudioStoreTypes = { FETCH_MORA_DATA: { action(payload: { accentPhrases: AccentPhrase[]; + engineKey: string; styleId: number; }): Promise; }; @@ -322,6 +338,7 @@ type AudioStoreTypes = { FETCH_AND_COPY_MORA_DATA: { action(payload: { accentPhrases: AccentPhrase[]; + engineKey: string; styleId: number; copyIndexes: number[]; }): Promise; @@ -461,12 +478,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: { @@ -603,6 +624,7 @@ type AudioCommandStoreTypes = { action(payload: { prevAudioKey: string; texts: string[]; + engineId: string; styleId: number; }): string[]; }; @@ -1135,7 +1157,9 @@ export type DictionaryStoreState = Record; type DictionaryStoreTypes = { LOAD_USER_DICT: { - action(): Promise>; + action(payload: { + engineKey: string; + }): Promise>; }; ADD_WORD: { action(payload: { diff --git a/src/views/Home.vue b/src/views/Home.vue index 2c7eb032ac..767fc2a97f 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -132,13 +132,13 @@ @@ -178,11 +178,13 @@ import { AudioItem, EngineState } from "@/store/type"; import { QResizeObserver, useQuasar } from "quasar"; import path from "path"; import { + CharacterInfo, HotkeyAction, HotkeyReturnType, SplitterPosition, } from "@/type/preload"; import { parseCombo, setHotkeyFunctions } from "@/store/setting"; +import { getFlattenCharacterInfos } from "@/store/audio"; export default defineComponent({ name: "Home", @@ -368,9 +370,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 +388,7 @@ export default defineComponent({ //パラメータ引き継ぎがONの場合は話速等のパラメータを引き継いでテキスト欄を作成する //パラメータ引き継ぎがOFFの場合、baseAudioItemがundefinedになっているのでパラメータ引き継ぎは行われない audioItem = await store.dispatch("GENERATE_AUDIO_ITEM", { + engineId, styleId, presetKey, baseAudioItem, @@ -468,7 +473,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,13 +483,20 @@ export default defineComponent({ // スタイルが複数あって未選択なキャラがいる場合はデフォルトスタイル選択ダイアログを表示 let isUnsetDefaultStyleIds = false; - if (characterInfos.value == undefined) throw new Error(); - for (const info of characterInfos.value) { - isUnsetDefaultStyleIds ||= - info.metas.styles.length > 1 && - (await store.dispatch("IS_UNSET_DEFAULT_STYLE_ID", { - speakerUuid: info.metas.speakerUuid, - })); + + for (const engineKey of store.state.engineKeys) { + const engineCharacterInfos: CharacterInfo[] | undefined = + store.state.characterInfos[engineKey]; + if (engineCharacterInfos === undefined) + throw new Error(`CharacterInfos not loaded for engine ${engineKey}`); + + for (const info of engineCharacterInfos) { + isUnsetDefaultStyleIds ||= + info.metas.styles.length > 1 && + (await store.dispatch("IS_UNSET_DEFAULT_STYLE_ID", { + speakerUuid: info.metas.speakerUuid, + })); + } } isDefaultStyleSelectDialogOpenComputed.value = isUnsetDefaultStyleIds; @@ -579,7 +591,9 @@ export default defineComponent({ }); // キャラクター並び替え - const characterInfos = computed(() => store.state.characterInfos); + const flattenCharacterInfos = computed(() => + getFlattenCharacterInfos(store.state) + ); const isCharacterOrderDialogOpenComputed = computed({ get: () => !store.state.isAcceptTermsDialogOpen && @@ -680,7 +694,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 a93d673073..4dcaf2d8e8 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: {}, @@ -136,6 +137,7 @@ describe("store/vuex.js test", () => { store.state.engineKeys.forEach((engineKey) => assert.equal(store.state.engineStates[engineKey], "STARTING") ); + assert.isObject(store.state.characterInfos); assert.isArray(store.state.defaultStyleIds); assert.isObject(store.state.audioItems); assert.isEmpty(store.state.audioItems);