Skip to content

Commit

Permalink
ENH: モーフィングUIの実装 (#1030)
Browse files Browse the repository at this point in the history
* ENH: audioStoreにモーフィング適用するための操作を追加

* FIX: MorphingInfo型からundefinedを除去

* ENH: モーフィングUIとプリセットを追加

* Update src/components/AudioInfo.vue

Co-authored-by: Hiroshiba <[email protected]>

* Update src/components/AudioInfo.vue

Co-authored-by: Hiroshiba <[email protected]>

* FIX: 変数名等を修正

* FIX: convertAudioQueryFromEditorToEngineを共通化

* ADD: モーフィングの設定が不正である場合エラーを返してその旨をダイアログに表示する

* FIX: GenerateAudioResultObjectを廃止して例外を投げるように変更

* FIX: PLAY_AUDIOのエラー処理を変更

* FIX: 設定でモーフィングが無効化されていてもモーフィングが行われるように変更

* FIXMEコメントの追加

* FIX: Errorの型名を変更

Co-authored-by: Hiroshiba <[email protected]>
  • Loading branch information
sabonerune and Hiroshiba authored Jan 16, 2023
1 parent cadd27e commit 21ff141
Show file tree
Hide file tree
Showing 15 changed files with 614 additions and 168 deletions.
9 changes: 8 additions & 1 deletion src/components/AudioDetail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
238 changes: 232 additions & 6 deletions src/components/AudioInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,92 @@
@pan="postPhonemeLengthSlider.qSliderProps.onPan"
/>
</div>
<div
v-if="shouldShowMorphing"
class="q-px-md"
:class="{
disabled: uiLocked,
}"
>
<q-separator class="q-mb-md" />
<span class="text-body1 q-mb-xs">モーフィング</span>
<div class="row no-wrap items-center">
<character-button
class="q-my-xs"
:character-infos="mophingTargetCharacters"
:show-engine-info="mophingTargetEngines.length >= 2"
:emptiable="true"
:ui-locked="uiLocked"
v-model:selected-voice="morphingTargetVoice"
/>
<div class="q-pl-xs overflow-hidden">
<div class="text-body2 text-no-wrap ellipsis overflow-hidden">
{{
morphingTargetCharacterInfo
? morphingTargetCharacterInfo.metas.speakerName
: "未設定"
}}
</div>
<div
v-if="
morphingTargetCharacterInfo &&
morphingTargetCharacterInfo.metas.styles.length >= 2
"
class="text-body2 text-no-wrap ellipsis overflow-hidden"
>
({{
morphingTargetStyleInfo
? morphingTargetStyleInfo.styleName
: undefined
}})
</div>
</div>
</div>
<div
v-if="!isSupportedMorphing"
class="text-warning"
style="font-size: 0.7rem"
>
非対応エンジンです
</div>
<div
v-else-if="isValidMorphingInfo"
class="text-warning"
style="font-size: 0.7rem"
>
無効な設定です
</div>
<div :class="{ disabled: morphingTargetStyleInfo == undefined }">
<span class="text-body1 q-mb-xs"
>割合
{{
morphingRateSlider.state.currentValue.value != undefined
? morphingRateSlider.state.currentValue.value.toFixed(2)
: undefined
}}</span
>
<q-slider
dense
snap
color="primary-light"
trackSize="2px"
:min="morphingRateSlider.qSliderProps.min.value"
:max="morphingRateSlider.qSliderProps.max.value"
:step="morphingRateSlider.qSliderProps.step.value"
:disable="
morphingRateSlider.qSliderProps.disable.value ||
morphingTargetStyleInfo == undefined
"
:model-value="morphingRateSlider.qSliderProps.modelValue.value"
@update:model-value="
morphingRateSlider.qSliderProps['onUpdate:modelValue']
"
@change="morphingRateSlider.qSliderProps.onChange"
@wheel="morphingRateSlider.qSliderProps.onWheel"
@pan="morphingRateSlider.qSliderProps.onPan"
/>
</div>
</div>
</div>
</template>

Expand All @@ -400,15 +486,17 @@ 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";
export default defineComponent({
name: "AudioInfo",
components: {
CharacterButton,
PresetManageDialog,
},
Expand Down Expand Up @@ -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: () =>
Expand Down Expand Up @@ -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
Expand All @@ -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<Preset, "name">)[];
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 = {
Expand Down Expand Up @@ -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,
};
});
Expand Down Expand Up @@ -895,6 +1111,7 @@ export default defineComponent({
setAudioVolumeScale,
setAudioPrePhonemeLength,
setAudioPostPhonemeLength,
setMorphingRate,
applyPreset,
enablePreset,
isRegisteredPreset,
Expand All @@ -920,6 +1137,15 @@ export default defineComponent({
volumeScaleSlider,
prePhonemeLengthSlider,
postPhonemeLengthSlider,
mophingTargetEngines,
shouldShowMorphing,
isSupportedMorphing,
isValidMorphingInfo,
mophingTargetCharacters,
morphingTargetVoice,
morphingTargetCharacterInfo,
morphingTargetStyleInfo,
morphingRateSlider,
handleChangeSpeedScaleInput,
handleChangePitchScaleInput,
handleChangeIntonationInput,
Expand Down
14 changes: 8 additions & 6 deletions src/components/CharacterButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
transition-show="none"
transition-hide="none"
>
<q-list>
<q-list style="min-width: max-content">
<q-item
v-if="selectedStyleInfo == undefined"
v-if="selectedStyleInfo == undefined && !emptiable"
class="row no-wrap items-center"
>
<span class="text-warning vertical-middle"
Expand Down Expand Up @@ -123,7 +123,7 @@
class="character-menu"
v-model="subMenuOpenFlags[characterIndex]"
>
<q-list>
<q-list style="min-width: max-content">
<q-item
v-for="(style, styleIndex) in characterInfo.metas.styles"
:key="styleIndex"
Expand Down Expand Up @@ -221,6 +221,7 @@ export default defineComponent({
const selectedCharacter = computed(() => {
const selectedVoice = props.selectedVoice;
if (selectedVoice == undefined) return undefined;
const character = props.characterInfos.find(
(characterInfo) =>
characterInfo.metas.speakerUuid === selectedVoice?.speakerId &&
Expand Down Expand Up @@ -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");
Expand Down
Loading

0 comments on commit 21ff141

Please sign in to comment.