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 @@
-
-
-
-
-
-
- 音声書き出し結果
-
-
-
-
-
-
- 失敗(書き込みエラー):
-
-
- {{ value.path }}
- 詳細:{{ value.message }}
-
-
-
-
- 失敗(エンジンエラー):
-
-
- {{ value }}
-
-
-
-
- 成功:
-
-
- {{ value }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ 音声書き出し結果
+
+
+
+
+
+
+ 失敗(書き込みエラー):
+
+
+ {{ value.path }}
+ 詳細:{{ value.message }}
+
+
+
+
+ 失敗(エンジンエラー):
+
+
+ {{ value.path }}
+ 詳細:{{ value.message }}
+
+
+
+
+ 成功:
+
+
+ {{ value }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 2cadf7463a..38b9768284 100644
--- a/src/type/preload.ts
+++ b/src/type/preload.ts
@@ -287,6 +287,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 = {
@@ -390,6 +398,7 @@ export type ThemeSetting = {
export type ExperimentalSetting = {
enablePreset: boolean;
enableInterrogativeUpspeak: boolean;
+ enableMorphing: boolean;
};
export const splitterPositionSchema = z.object({
@@ -448,6 +457,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({}),
@@ -460,6 +477,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: {