From 21ff14121d80f2c3fbb9bd215539404716f73902 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Mon, 16 Jan 2023 20:25:34 +0900 Subject: [PATCH] =?UTF-8?q?ENH:=20=E3=83=A2=E3=83=BC=E3=83=95=E3=82=A3?= =?UTF-8?q?=E3=83=B3=E3=82=B0UI=E3=81=AE=E5=AE=9F=E8=A3=85=20(#1030)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ENH: audioStoreにモーフィング適用するための操作を追加 * FIX: MorphingInfo型からundefinedを除去 * ENH: モーフィングUIとプリセットを追加 * Update src/components/AudioInfo.vue Co-authored-by: Hiroshiba * Update src/components/AudioInfo.vue Co-authored-by: Hiroshiba * FIX: 変数名等を修正 * FIX: convertAudioQueryFromEditorToEngineを共通化 * ADD: モーフィングの設定が不正である場合エラーを返してその旨をダイアログに表示する * FIX: GenerateAudioResultObjectを廃止して例外を投げるように変更 * FIX: PLAY_AUDIOのエラー処理を変更 * FIX: 設定でモーフィングが無効化されていてもモーフィングが行われるように変更 * FIXMEコメントの追加 * FIX: Errorの型名を変更 Co-authored-by: Hiroshiba --- src/components/AudioDetail.vue | 9 +- src/components/AudioInfo.vue | 238 +++++++++++++++++++++- src/components/CharacterButton.vue | 14 +- src/components/Dialog.ts | 26 ++- src/components/DictionaryManageDialog.vue | 14 +- src/components/HeaderBar.vue | 8 +- src/components/SaveAllResultDialog.vue | 201 +++++++++--------- src/components/SettingDialog.vue | 24 +++ src/helpers/previewSliderHelper.ts | 1 + src/store/audio.ts | 183 +++++++++++++---- src/store/project.ts | 10 + src/store/setting.ts | 1 + src/store/type.ts | 34 +++- src/type/preload.ts | 18 ++ tests/unit/store/Vuex.spec.ts | 1 + 15 files changed, 614 insertions(+), 168 deletions(-) diff --git a/src/components/AudioDetail.vue b/src/components/AudioDetail.vue index 9c40ac398b..ded5d1784c 100644 --- a/src/components/AudioDetail.vue +++ b/src/components/AudioDetail.vue @@ -503,9 +503,16 @@ export default defineComponent({ audioKey: props.activeAudioKey, }); } catch (e) { + let msg: string | undefined; + // FIXME: GENERATE_AUDIO_FROM_AUDIO_ITEMのエラーを変えた場合変更する + if (e instanceof Error && e.message === "VALID_MOPHING_ERROR") { + msg = "モーフィングの設定が無効です。"; + } else { + window.electron.logError(e); + } $q.dialog({ title: "再生に失敗しました", - message: "エンジンの再起動をお試しください。", + message: msg ?? "エンジンの再起動をお試しください。", ok: { label: "閉じる", flat: true, diff --git a/src/components/AudioInfo.vue b/src/components/AudioInfo.vue index d1f40c6d3b..56d6d8b5b5 100644 --- a/src/components/AudioInfo.vue +++ b/src/components/AudioInfo.vue @@ -392,6 +392,92 @@ @pan="postPhonemeLengthSlider.qSliderProps.onPan" /> +
+ + モーフィング +
+ +
+
+ {{ + morphingTargetCharacterInfo + ? morphingTargetCharacterInfo.metas.speakerName + : "未設定" + }} +
+
+ ({{ + morphingTargetStyleInfo + ? morphingTargetStyleInfo.styleName + : undefined + }}) +
+
+
+
+ 非対応エンジンです +
+
+ 無効な設定です +
+
+ 割合 + {{ + morphingRateSlider.state.currentValue.value != undefined + ? morphingRateSlider.state.currentValue.value.toFixed(2) + : undefined + }} + +
+
@@ -400,8 +486,9 @@ import { computed, defineComponent, ref } from "vue"; import { QSelectProps } from "quasar"; import { useStore } from "@/store"; -import { Preset } from "@/type/preload"; +import { MorphingInfo, Preset, Voice } from "@/type/preload"; import { previewSliderHelper } from "@/helpers/previewSliderHelper"; +import CharacterButton from "./CharacterButton.vue"; import PresetManageDialog from "./PresetManageDialog.vue"; import { EngineManifest } from "@/openapi"; @@ -409,6 +496,7 @@ export default defineComponent({ name: "AudioInfo", components: { + CharacterButton, PresetManageDialog, }, @@ -485,6 +573,22 @@ export default defineComponent({ }); }; + const setMorphingRate = (rate: number) => { + const info = audioItem.value.morphingInfo; + if (info == undefined) { + throw new Error("audioItem.value.morphingInfo == undefined"); + } + store.dispatch("COMMAND_SET_MORPHING_INFO", { + audioKey: props.activeAudioKey, + morphingInfo: { + rate, + targetEngineId: info.targetEngineId, + targetSpeakerId: info.targetSpeakerId, + targetStyleId: info.targetStyleId, + }, + }); + }; + const speedScaleSlider = previewSliderHelper({ modelValue: () => query.value?.speedScale ?? null, disable: () => @@ -550,6 +654,89 @@ export default defineComponent({ scrollMinStep: () => 0.01, }); + // モーフィング + const shouldShowMorphing = computed( + () => store.state.experimentalSetting.enableMorphing + ); + + const isSupportedMorphing = computed( + () => supportedFeatures.value?.synthesisMorphing + ); + + const isValidMorphingInfo = computed(() => { + if (audioItem.value.morphingInfo == undefined) return false; + return !store.getters.VALID_MOPHING_INFO(audioItem.value); + }); + + const mophingTargetEngines = store.getters.MORPHING_SUPPORTED_ENGINES; + + const mophingTargetCharacters = computed(() => { + const allCharacters = store.getters.GET_ORDERED_ALL_CHARACTER_INFOS; + return allCharacters + .map((character) => { + const targetStyles = character.metas.styles.filter((style) => + mophingTargetEngines.includes(style.engineId) + ); + character.metas.styles = targetStyles; + return character; + }) + .filter((characters) => characters.metas.styles.length >= 1); + }); + + const morphingTargetVoice = computed({ + get() { + const morphingInfo = audioItem.value.morphingInfo; + if (morphingInfo == undefined) return undefined; + return { + engineId: morphingInfo.targetEngineId, + speakerId: morphingInfo.targetSpeakerId, + styleId: morphingInfo.targetStyleId, + }; + }, + set(voice: Voice | undefined) { + const morphingInfo = + voice != undefined + ? { + rate: audioItem.value.morphingInfo?.rate ?? 0.5, + targetEngineId: voice.engineId, + targetSpeakerId: voice.speakerId, + targetStyleId: voice.styleId, + } + : undefined; + store.dispatch("COMMAND_SET_MORPHING_INFO", { + audioKey: props.activeAudioKey, + morphingInfo, + }); + }, + }); + + const morphingTargetCharacterInfo = computed(() => + mophingTargetCharacters.value.find( + (character) => + character.metas.speakerUuid === morphingTargetVoice.value?.speakerId + ) + ); + + const morphingTargetStyleInfo = computed(() => { + const targetVoice = morphingTargetVoice.value; + return morphingTargetCharacterInfo.value?.metas.styles.find( + (style) => + style.engineId === targetVoice?.engineId && + style.styleId === targetVoice.styleId + ); + }); + + const morphingRateSlider = previewSliderHelper({ + modelValue: () => audioItem.value.morphingInfo?.rate ?? null, + disable: () => uiLocked.value, + onChange: setMorphingRate, + max: () => 1, + min: () => 0, + step: () => 0.01, + scrollStep: () => 0.1, + scrollMinStep: () => 0.01, + }); + // プリセット const enablePreset = computed( () => store.state.experimentalSetting.enablePreset @@ -572,13 +759,30 @@ export default defineComponent({ if (audioPresetKey.value == undefined) throw new Error("audioPresetKey is undefined"); // 次のコードが何故かコンパイルエラーになるチェック const preset = presetItems.value[audioPresetKey.value]; - const { name: _, ...presetParts } = preset; + const { name: _, morphingInfo, ...presetParts } = preset; // 入力パラメータと比較 - const keys = Object.keys(presetParts) as (keyof Omit)[]; - return keys.some( - (key) => presetParts[key] !== presetPartsFromParameter.value[key] - ); + const keys = Object.keys(presetParts) as (keyof Omit< + Preset, + "name" | "morphingInfo" + >)[]; + if ( + keys.some( + (key) => presetParts[key] !== presetPartsFromParameter.value[key] + ) + ) + return true; + const morphingInfoFromParameter = + presetPartsFromParameter.value.morphingInfo; + if (morphingInfo && morphingInfoFromParameter) { + const morphingInfoKeys = Object.keys( + morphingInfo + ) as (keyof MorphingInfo)[]; + return morphingInfoKeys.some( + (key) => morphingInfo[key] !== morphingInfoFromParameter[key] + ); + } + return morphingInfo != morphingInfoFromParameter; }); type PresetSelectModelType = { @@ -737,6 +941,18 @@ export default defineComponent({ volumeScale: volumeScaleSlider.state.currentValue.value, prePhonemeLength: prePhonemeLengthSlider.state.currentValue.value, postPhonemeLength: postPhonemeLengthSlider.state.currentValue.value, + morphingInfo: + morphingTargetStyleInfo.value && + morphingTargetCharacterInfo.value && + morphingRateSlider.state.currentValue.value != undefined // FIXME: ifでチェックしてthrowする + ? { + rate: morphingRateSlider.state.currentValue.value, + targetEngineId: morphingTargetStyleInfo.value.engineId, + targetSpeakerId: + morphingTargetCharacterInfo.value.metas.speakerUuid, + targetStyleId: morphingTargetStyleInfo.value.styleId, + } + : undefined, }; }); @@ -895,6 +1111,7 @@ export default defineComponent({ setAudioVolumeScale, setAudioPrePhonemeLength, setAudioPostPhonemeLength, + setMorphingRate, applyPreset, enablePreset, isRegisteredPreset, @@ -920,6 +1137,15 @@ export default defineComponent({ volumeScaleSlider, prePhonemeLengthSlider, postPhonemeLengthSlider, + mophingTargetEngines, + shouldShowMorphing, + isSupportedMorphing, + isValidMorphingInfo, + mophingTargetCharacters, + morphingTargetVoice, + morphingTargetCharacterInfo, + morphingTargetStyleInfo, + morphingRateSlider, handleChangeSpeedScaleInput, handleChangePitchScaleInput, handleChangeIntonationInput, diff --git a/src/components/CharacterButton.vue b/src/components/CharacterButton.vue index fffc6f638c..66639a0e1a 100644 --- a/src/components/CharacterButton.vue +++ b/src/components/CharacterButton.vue @@ -24,9 +24,9 @@ transition-show="none" transition-hide="none" > - + - + { const selectedVoice = props.selectedVoice; + if (selectedVoice == undefined) return undefined; const character = props.characterInfos.find( (characterInfo) => characterInfo.metas.speakerUuid === selectedVoice?.speakerId && @@ -261,9 +262,10 @@ export default defineComponent({ (x) => x.speakerUuid === speakerUuid )?.defaultStyleId; - const defaultStyle = characterInfo?.metas.styles.find( - (style) => style.styleId === defaultStyleId - ); + const defaultStyle = + characterInfo?.metas.styles.find( + (style) => style.styleId === defaultStyleId + ) ?? characterInfo?.metas.styles[0]; // デフォルトのスタイルIDが見つからない場合stylesの先頭を選択する if (defaultStyle == undefined) throw new Error("defaultStyle == undefined"); diff --git a/src/components/Dialog.ts b/src/components/Dialog.ts index 9ac6fd05fa..dc82c48bd1 100644 --- a/src/components/Dialog.ts +++ b/src/components/Dialog.ts @@ -2,7 +2,7 @@ import { Encoding as EncodingType } from "@/type/preload"; import { AllActions, SaveResultObject, - WriteErrorTypeForSaveAllResultDialog, + ErrorTypeForSaveAllResultDialog, } from "@/store/type"; import SaveAllResultDialog from "@/components/SaveAllResultDialog.vue"; import { QVueGlobals } from "quasar"; @@ -45,8 +45,12 @@ export async function generateAndSaveOneAudioWithDialog({ } break; case "ENGINE_ERROR": - msg = - "エンジンのエラーによって失敗しました。エンジンの再起動をお試しください。"; + if (result.errorMessage) { + msg = result.errorMessage; + } else { + msg = + "エンジンのエラーによって失敗しました。エンジンの再起動をお試しください。"; + } break; } quasarDialog({ @@ -82,8 +86,8 @@ export async function generateAndSaveAllAudioWithDialog({ ); const successArray: Array = []; - const writeErrorArray: Array = []; - const engineErrorArray: Array = []; + const writeErrorArray: Array = []; + const engineErrorArray: Array = []; if (result) { for (const item of result) { @@ -105,7 +109,7 @@ export async function generateAndSaveAllAudioWithDialog({ writeErrorArray.push({ path: path, message: msg }); break; case "ENGINE_ERROR": - engineErrorArray.push(path); + engineErrorArray.push({ path: path, message: msg }); break; } } @@ -154,15 +158,19 @@ export async function generateAndConnectAndSaveAudioWithDialog({ let msg = ""; switch (result.result) { case "WRITE_ERROR": - if (result.errorMessage) { + if (result.errorMessage != undefined) { msg = result.errorMessage; } else { msg = "何らかの理由で書き出しに失敗しました。ログを参照してください。"; } break; case "ENGINE_ERROR": - msg = - "エンジンのエラーによって失敗しました。エンジンの再起動をお試しください。"; + if (result.errorMessage != undefined) { + msg = result.errorMessage; + } else { + msg = + "エンジンのエラーによって失敗しました。エンジンの再起動をお試しください。"; + } break; } diff --git a/src/components/DictionaryManageDialog.vue b/src/components/DictionaryManageDialog.vue index 690596e430..b7a42fc76b 100644 --- a/src/components/DictionaryManageDialog.vue +++ b/src/components/DictionaryManageDialog.vue @@ -483,12 +483,14 @@ export default defineComponent({ audioItem, }); if (!blob) { - blob = await createUILockAction( - store.dispatch("GENERATE_AUDIO_FROM_AUDIO_ITEM", { - audioItem, - }) - ); - if (!blob) { + try { + blob = await createUILockAction( + store.dispatch("GENERATE_AUDIO_FROM_AUDIO_ITEM", { + audioItem, + }) + ); + } catch (e) { + window.electron.logError(e); nowGenerating.value = false; $q.dialog({ title: "生成に失敗しました", diff --git a/src/components/HeaderBar.vue b/src/components/HeaderBar.vue index 60e9800fce..d3880b9400 100644 --- a/src/components/HeaderBar.vue +++ b/src/components/HeaderBar.vue @@ -109,10 +109,14 @@ export default defineComponent({ const playContinuously = async () => { try { await store.dispatch("PLAY_CONTINUOUSLY_AUDIO"); - } catch { + } catch (e) { + let msg: string | undefined; + if (e instanceof Error && e.message !== "") { + msg = e.message; + } $q.dialog({ title: "再生に失敗しました", - message: "エンジンの再起動をお試しください。", + message: msg ?? "エンジンの再起動をお試しください。", ok: { label: "閉じる", flat: true, diff --git a/src/components/SaveAllResultDialog.vue b/src/components/SaveAllResultDialog.vue index c76b6036cf..66ae8f8c66 100644 --- a/src/components/SaveAllResultDialog.vue +++ b/src/components/SaveAllResultDialog.vue @@ -1,94 +1,107 @@ - - - - - + + + + + diff --git a/src/components/SettingDialog.vue b/src/components/SettingDialog.vue index bf1ce2afda..2a15acbef9 100644 --- a/src/components/SettingDialog.vue +++ b/src/components/SettingDialog.vue @@ -623,6 +623,30 @@ > + +
モーフィング機能
+
+ + + 2人の話者でモーフィングした音声を合成する + + +
+ + + +
diff --git a/src/helpers/previewSliderHelper.ts b/src/helpers/previewSliderHelper.ts index 17f96b072c..5e5ecdf119 100644 --- a/src/helpers/previewSliderHelper.ts +++ b/src/helpers/previewSliderHelper.ts @@ -25,6 +25,7 @@ export type PreviewSliderHelper = { max: Ref; step: Ref; disable: Ref; + modelValue: Ref; "onUpdate:modelValue": (value: number) => void; onChange: (value: number) => void; onWheel: (event: Events["onWheel"]) => void; diff --git a/src/store/audio.ts b/src/store/audio.ts index efdbb4fc44..7c1259f447 100644 --- a/src/store/audio.ts +++ b/src/store/audio.ts @@ -18,6 +18,7 @@ import { DefaultStyleId, Encoding as EncodingType, MoraDataType, + MorphingInfo, StyleInfo, WriteFileErrorResult, } from "@/type/preload"; @@ -51,6 +52,7 @@ async function generateUniqueIdAndQuery( audioQuery, audioItem.engineId, audioItem.styleId, + audioItem.morphingInfo, state.experimentalSetting.enableInterrogativeUpspeak, // このフラグが違うと、同じAudioQueryで違う音声が生成されるので追加 ]) ); @@ -478,6 +480,7 @@ export const audioStore = createPartialStore({ audioItem.query.outputSamplingRate = baseAudioItem.query.outputSamplingRate; audioItem.query.outputStereo = baseAudioItem.query.outputStereo; + audioItem.morphingInfo = baseAudioItem.morphingInfo; } return audioItem; }, @@ -672,6 +675,41 @@ export const audioStore = createPartialStore({ }, }, + SET_MORPHING_INFO: { + mutation( + state, + { + audioKey, + morphingInfo, + }: { audioKey: string; morphingInfo: MorphingInfo | undefined } + ) { + const item = state.audioItems[audioKey]; + item.morphingInfo = morphingInfo; + }, + }, + + MORPHING_SUPPORTED_ENGINES: { + getter: (state) => + state.engineIds.filter( + (engineId) => + state.engineManifests[engineId].supportedFeatures?.synthesisMorphing + ), + }, + + VALID_MOPHING_INFO: { + getter: (_, getters) => (audioItem: AudioItem) => { + if ( + audioItem.morphingInfo == undefined || + audioItem.engineId == undefined + ) + return false; + return ( + getters.MORPHING_SUPPORTED_ENGINES.includes(audioItem.engineId) && + audioItem.engineId === audioItem.morphingInfo.targetEngineId + ); + }, + }, + SET_AUDIO_QUERY: { mutation( state, @@ -864,7 +902,7 @@ export const audioStore = createPartialStore({ if (presetItem == undefined) return; // Filter name property from presetItem in order to extract audioInfos. - const { name: _, ...presetAudioInfos } = presetItem; + const { name: _, morphingInfo, ...presetAudioInfos } = presetItem; // Type Assertion const audioInfos: Omit< @@ -873,6 +911,8 @@ export const audioStore = createPartialStore({ > = presetAudioInfos; audioItem.query = { ...audioItem.query, ...audioInfos }; + + audioItem.morphingInfo = morphingInfo; }, }, @@ -1037,42 +1077,57 @@ export const audioStore = createPartialStore({ GENERATE_AUDIO_FROM_AUDIO_ITEM: { action: createUILockAction( - async ({ dispatch, state }, { audioItem }: { audioItem: AudioItem }) => { + async ( + { dispatch, getters, state }, + { audioItem }: { audioItem: AudioItem } + ) => { const engineId = audioItem.engineId; if (engineId === undefined) - throw new Error(`engineId is not defined for audioItem`); + throw new Error("engineId is not defined for audioItem"); const [id, audioQuery] = await generateUniqueIdAndQuery( state, audioItem ); + if (audioQuery == undefined) + throw new Error("audioQuery is not defined for audioItem"); + const speaker = audioItem.styleId; - if (audioQuery == undefined || speaker == undefined) { - return null; - } + if (speaker == undefined) + throw new Error("speaker is not defined for audioItem"); + + const engineAudioQuery = convertAudioQueryFromEditorToEngine( + audioQuery, + state.engineManifests[engineId].defaultSamplingRate + ); return dispatch("INSTANTIATE_ENGINE_CONNECTOR", { engineId, - }) - .then((instance) => - instance.invoke("synthesisSynthesisPost")({ - audioQuery: convertAudioQueryFromEditorToEngine( - audioQuery, - state.engineManifests[engineId].defaultSamplingRate - ), + }).then(async (instance) => { + let blob: Blob; + // FIXME: モーフィングが設定で無効化されていてもモーフィングが行われるので気づけるUIを作成する + if (audioItem.morphingInfo != undefined) { + if (!getters.VALID_MOPHING_INFO(audioItem)) + throw new Error("VALID_MOPHING_ERROR"); //FIXME: エラーを変更した場合ハンドリング部分も修正する + blob = await instance.invoke( + "synthesisMorphingSynthesisMorphingPost" + )({ + audioQuery: engineAudioQuery, + baseSpeaker: speaker, + targetSpeaker: audioItem.morphingInfo.targetStyleId, + morphRate: audioItem.morphingInfo.rate, + }); + } else { + blob = await instance.invoke("synthesisSynthesisPost")({ + audioQuery: engineAudioQuery, speaker, enableInterrogativeUpspeak: state.experimentalSetting.enableInterrogativeUpspeak, - }) - ) - .then(async (blob) => { - audioBlobCache[id] = blob; - return blob; - }) - .catch((e) => { - window.electron.logError(e); - return null; - }); + }); + } + audioBlobCache[id] = blob; + return blob; + }); } ), }, @@ -1147,9 +1202,21 @@ export const audioStore = createPartialStore({ let blob = await dispatch("GET_AUDIO_CACHE", { audioKey }); if (!blob) { - blob = await dispatch("GENERATE_AUDIO", { audioKey }); - if (!blob) { - return { result: "ENGINE_ERROR", path: filePath }; + try { + blob = await dispatch("GENERATE_AUDIO", { audioKey }); + } catch (e) { + let errorMessage = undefined; + // FIXME: GENERATE_AUDIO_FROM_AUDIO_ITEMのエラーを変えた場合変更する + if (e instanceof Error && e.message === "VALID_MOPHING_ERROR") { + errorMessage = "モーフィングの設定が無効です。"; + } else { + window.electron.logError(e); + } + return { + result: "ENGINE_ERROR", + path: filePath, + errorMessage, + }; } } @@ -1344,11 +1411,24 @@ export const audioStore = createPartialStore({ for (const audioKey of state.audioKeys) { let blob = await dispatch("GET_AUDIO_CACHE", { audioKey }); if (!blob) { - blob = await dispatch("GENERATE_AUDIO", { audioKey }); - callback?.(++finishedCount, totalCount); - } - if (blob === null) { - return { result: "ENGINE_ERROR", path: filePath }; + try { + blob = await dispatch("GENERATE_AUDIO", { audioKey }); + } catch (e) { + let errorMessage = undefined; + // FIXME: GENERATE_AUDIO_FROM_AUDIO_ITEMのエラーを変えた場合変更する + if (e instanceof Error && e.message === "VALID_MOPHING_ERROR") { + errorMessage = "モーフィングの設定が無効です。"; + } else { + window.electron.logError(e); + } + return { + result: "ENGINE_ERROR", + path: filePath, + errorMessage, + }; + } finally { + callback?.(++finishedCount, totalCount); + } } const encodedBlob = await base64Encoder(blob); if (encodedBlob === undefined) { @@ -1550,16 +1630,16 @@ export const audioStore = createPartialStore({ audioKey, nowGenerating: true, }); - blob = await withProgress( - dispatch("GENERATE_AUDIO", { audioKey }), - dispatch - ); - commit("SET_AUDIO_NOW_GENERATING", { - audioKey, - nowGenerating: false, - }); - if (!blob) { - throw new Error(); + try { + blob = await withProgress( + dispatch("GENERATE_AUDIO", { audioKey }), + dispatch + ); + } finally { + commit("SET_AUDIO_NOW_GENERATING", { + audioKey, + nowGenerating: false, + }); } } @@ -2504,6 +2584,27 @@ export const audioCommandStore = transformCommandStore( }, }, + COMMAND_SET_MORPHING_INFO: { + mutation( + draft, + payload: { + audioKey: string; + morphingInfo: MorphingInfo | undefined; + } + ) { + audioStore.mutations.SET_MORPHING_INFO(draft, payload); + }, + action( + { commit }, + payload: { + audioKey: string; + morphingInfo: MorphingInfo | undefined; + } + ) { + commit("COMMAND_SET_MORPHING_INFO", payload); + }, + }, + COMMAND_SET_AUDIO_PRESET: { mutation( draft, diff --git a/src/store/project.ts b/src/store/project.ts index f11bdfbc2d..bb7830da3a 100755 --- a/src/store/project.ts +++ b/src/store/project.ts @@ -419,6 +419,15 @@ const audioQuerySchema = { }, } as const; +const morphingInfoSchema = { + properties: { + rate: { type: "float32" }, + targetEngineId: { type: "string" }, + targetSpeakerId: { type: "string" }, + targetStyleId: { type: "int32" }, + }, +} as const; + const audioItemSchema = { properties: { text: { type: "string" }, @@ -428,6 +437,7 @@ const audioItemSchema = { styleId: { type: "int32" }, query: audioQuerySchema, presetKey: { type: "string" }, + morphingInfo: morphingInfoSchema, }, } as const; diff --git a/src/store/setting.ts b/src/store/setting.ts index 9f72af920d..50c0c115fd 100644 --- a/src/store/setting.ts +++ b/src/store/setting.ts @@ -44,6 +44,7 @@ export const settingStoreState: SettingStoreState = { experimentalSetting: { enablePreset: false, enableInterrogativeUpspeak: false, + enableMorphing: false, }, splitTextWhenPaste: "PERIOD_AND_NEW_LINE", splitterPosition: { diff --git a/src/store/type.ts b/src/store/type.ts index 0707b666f4..698700fec3 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -29,6 +29,7 @@ import { ToolbarSetting, UpdateInfo, Preset, + MorphingInfo, ActivePointScrollMode, EngineInfo, SplitTextWhenPasteType, @@ -54,6 +55,7 @@ export type AudioItem = { styleId?: number; query?: EditorAudioQuery; presetKey?: string; + morphingInfo?: MorphingInfo; }; export type AudioState = { @@ -78,7 +80,7 @@ export type SaveResultObject = { path: string | undefined; errorMessage?: string; }; -export type WriteErrorTypeForSaveAllResultDialog = { +export type ErrorTypeForSaveAllResultDialog = { path: string; message: string; }; @@ -265,6 +267,21 @@ export type AudioStoreTypes = { mutation: { audioKey: string; postPhonemeLength: number }; }; + SET_MORPHING_INFO: { + mutation: { + audioKey: string; + morphingInfo: MorphingInfo | undefined; + }; + }; + + MORPHING_SUPPORTED_ENGINES: { + getter: string[]; + }; + + VALID_MOPHING_INFO: { + getter(audioItem: AudioItem): boolean; + }; + SET_AUDIO_QUERY: { mutation: { audioKey: string; audioQuery: AudioQuery }; action(payload: { audioKey: string; audioQuery: AudioQuery }): void; @@ -343,11 +360,11 @@ export type AudioStoreTypes = { }; GENERATE_AUDIO: { - action(payload: { audioKey: string }): Promise; + action(payload: { audioKey: string }): Promise; }; GENERATE_AUDIO_FROM_AUDIO_ITEM: { - action(payload: { audioItem: AudioItem }): Blob | null; + action(payload: { audioItem: AudioItem }): Blob; }; CONNECT_AUDIO: { @@ -581,6 +598,17 @@ export type AudioCommandStoreTypes = { action(payload: { audioKey: string; postPhonemeLength: number }): void; }; + COMMAND_SET_MORPHING_INFO: { + mutation: { + audioKey: string; + morphingInfo: MorphingInfo | undefined; + }; + action(payload: { + audioKey: string; + morphingInfo: MorphingInfo | undefined; + }): void; + }; + COMMAND_SET_AUDIO_PRESET: { mutation: { audioKey: string; diff --git a/src/type/preload.ts b/src/type/preload.ts index 6f124becd9..3edd9378ac 100644 --- a/src/type/preload.ts +++ b/src/type/preload.ts @@ -294,6 +294,14 @@ export type Preset = { volumeScale: number; prePhonemeLength: number; postPhonemeLength: number; + morphingInfo?: MorphingInfo; +}; + +export type MorphingInfo = { + rate: number; + targetEngineId: string; + targetSpeakerId: string; + targetStyleId: number; }; export type PresetConfig = { @@ -397,6 +405,7 @@ export type ThemeSetting = { export type ExperimentalSetting = { enablePreset: boolean; enableInterrogativeUpspeak: boolean; + enableMorphing: boolean; }; export const splitterPositionSchema = z.object({ @@ -455,6 +464,14 @@ export const electronStoreSchema = z volumeScale: z.number(), prePhonemeLength: z.number(), postPhonemeLength: z.number(), + morphingInfo: z + .object({ + rate: z.number(), + targetEngineId: z.string().uuid(), + targetSpeakerId: z.string().uuid(), + targetStyleId: z.number(), + }) + .optional(), }) ) .default({}), @@ -467,6 +484,7 @@ export const electronStoreSchema = z .object({ enablePreset: z.boolean().default(false), enableInterrogativeUpspeak: z.boolean().default(false), + enableMorphing: z.boolean().default(false), }) .passthrough() .default({}), diff --git a/tests/unit/store/Vuex.spec.ts b/tests/unit/store/Vuex.spec.ts index 4b89c8e28b..45dda25715 100644 --- a/tests/unit/store/Vuex.spec.ts +++ b/tests/unit/store/Vuex.spec.ts @@ -114,6 +114,7 @@ describe("store/vuex.js test", () => { experimentalSetting: { enablePreset: false, enableInterrogativeUpspeak: false, + enableMorphing: false, }, splitTextWhenPaste: "PERIOD_AND_NEW_LINE", splitterPosition: {