Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

複数選択:キャラクターを変更できるように #1546

Merged
merged 9 commits into from
Sep 19, 2023
10 changes: 6 additions & 4 deletions src/components/AudioCell.vue
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ const userOrderedCharacterInfos = computed(() => {
throw new Error("USER_ORDERED_CHARACTER_INFOS == undefined");
return infos;
});
const isInitializingSpeaker = computed(
() => store.state.audioKeyInitializingSpeaker === props.audioKey
const isInitializingSpeaker = computed(() =>
store.state.audioKeysWithInitializingSpeaker.includes(props.audioKey)
);
const audioItem = computed(() => store.state.audioItems[props.audioKey]);

Expand Down Expand Up @@ -262,8 +262,10 @@ const selectedVoice = computed<Voice | undefined>({
},
set(voice: Voice | undefined) {
if (voice == undefined) return;
store.dispatch("COMMAND_CHANGE_VOICE", {
audioKey: props.audioKey,
store.dispatch("COMMAND_MULTI_CHANGE_VOICE", {
audioKeys: isMultiSelectEnabled.value
? store.getters.SELECTED_AUDIO_KEYS
: [props.audioKey],
voice,
});
},
Expand Down
5 changes: 4 additions & 1 deletion src/components/CharacterPortrait.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ const portraitPath = computed(

const isInitializingSpeaker = computed(() => {
const activeAudioKey = store.getters.ACTIVE_AUDIO_KEY;
return store.state.audioKeyInitializingSpeaker === activeAudioKey;
return (
activeAudioKey &&
store.state.audioKeysWithInitializingSpeaker.includes(activeAudioKey)
);
});

const isMultipleEngine = computed(() => store.state.engineIds.length > 1);
Expand Down
2 changes: 1 addition & 1 deletion src/components/SettingDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1089,7 +1089,7 @@ const updateAudioOutputDevices = async () => {
return { label: device.label, key: device.deviceId };
});
};
navigator.mediaDevices.addEventListener(
navigator.mediaDevices?.addEventListener(
"devicechange",
updateAudioOutputDevices
);
Expand Down
172 changes: 90 additions & 82 deletions src/store/audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ const getAudioElement = (() => {

export const audioStoreState: AudioStoreState = {
characterInfos: {},
audioKeysWithInitializingSpeaker: [],
morphableTargetsInfo: {},
audioItems: {},
audioKeys: [],
Expand Down Expand Up @@ -507,30 +508,30 @@ export const audioStore = createPartialStore<AudioStoreTypes>({
/**
* AudioItemに設定される話者(スタイルID)に対してエンジン側の初期化を行い、即座に音声合成ができるようにする。
*/
async action({ commit, dispatch }, { engineId, audioKey, styleId }) {
async action({ commit, dispatch }, { engineId, audioKeys, styleId }) {
const isInitialized = await dispatch("IS_INITIALIZED_ENGINE_SPEAKER", {
engineId,
styleId,
});
if (isInitialized) return;

commit("SET_AUDIO_KEY_INITIALIZING_SPEAKER", {
audioKey,
commit("SET_AUDIO_KEYS_WITH_INITIALIZING_SPEAKER", {
audioKeys,
});
await dispatch("INITIALIZE_ENGINE_SPEAKER", {
engineId,
styleId,
}).finally(() => {
commit("SET_AUDIO_KEY_INITIALIZING_SPEAKER", {
audioKey: undefined,
commit("SET_AUDIO_KEYS_WITH_INITIALIZING_SPEAKER", {
audioKeys: [],
});
});
},
},

SET_AUDIO_KEY_INITIALIZING_SPEAKER: {
mutation(state, { audioKey }: { audioKey?: AudioKey }) {
state.audioKeyInitializingSpeaker = audioKey;
SET_AUDIO_KEYS_WITH_INITIALIZING_SPEAKER: {
mutation(state, { audioKeys }: { audioKeys: AudioKey[] }) {
state.audioKeysWithInitializingSpeaker = audioKeys;
},
},

Expand Down Expand Up @@ -2076,10 +2077,10 @@ export const audioCommandStore = transformCommandStore(
},
},

COMMAND_CHANGE_VOICE: {
COMMAND_MULTI_CHANGE_VOICE: {
mutation(
draft,
payload: { audioKey: AudioKey; voice: Voice } & (
payload: { audioKeys: AudioKey[]; voice: Voice } & (
| { update: "RollbackStyleId" }
| {
update: "AccentPhrases";
Expand All @@ -2091,93 +2092,100 @@ export const audioCommandStore = transformCommandStore(
}
)
) {
audioStore.mutations.SET_AUDIO_VOICE(draft, {
audioKey: payload.audioKey,
voice: payload.voice,
});
for (const audioKey of payload.audioKeys) {
audioStore.mutations.SET_AUDIO_VOICE(draft, {
audioKey,
voice: payload.voice,
});
}

if (payload.update === "RollbackStyleId") return;

const presetKey = draft.audioItems[payload.audioKey].presetKey;

const { nextPresetKey, shouldApplyPreset } = determineNextPresetKey(
draft,
payload.voice,
presetKey,
"changeVoice"
);
for (const audioKey of payload.audioKeys) {
const presetKey = draft.audioItems[audioKey].presetKey;

audioStore.mutations.SET_AUDIO_PRESET_KEY(draft, {
audioKey: payload.audioKey,
presetKey: nextPresetKey,
});
const { nextPresetKey, shouldApplyPreset } = determineNextPresetKey(
draft,
payload.voice,
presetKey,
"changeVoice"
);

if (payload.update == "AccentPhrases") {
audioStore.mutations.SET_ACCENT_PHRASES(draft, {
audioKey: payload.audioKey,
accentPhrases: payload.accentPhrases,
});
} else if (payload.update == "AudioQuery") {
audioStore.mutations.SET_AUDIO_QUERY(draft, {
audioKey: payload.audioKey,
audioQuery: payload.query,
audioStore.mutations.SET_AUDIO_PRESET_KEY(draft, {
audioKey,
presetKey: nextPresetKey,
});
}

if (shouldApplyPreset) {
audioStore.mutations.APPLY_AUDIO_PRESET(draft, {
audioKey: payload.audioKey,
});
if (payload.update == "AccentPhrases") {
audioStore.mutations.SET_ACCENT_PHRASES(draft, {
audioKey,
accentPhrases: payload.accentPhrases,
});
} else if (payload.update == "AudioQuery") {
audioStore.mutations.SET_AUDIO_QUERY(draft, {
audioKey,
audioQuery: payload.query,
});
}

if (shouldApplyPreset) {
audioStore.mutations.APPLY_AUDIO_PRESET(draft, {
audioKey,
});
}
}
},
async action(
{ state, dispatch, commit },
{ audioKey, voice }: { audioKey: AudioKey; voice: Voice }
{ audioKeys, voice }: { audioKeys: AudioKey[]; voice: Voice }
) {
const query = state.audioItems[audioKey].query;
const engineId = voice.engineId;
const styleId = voice.styleId;
try {
await dispatch("SETUP_SPEAKER", { audioKey, engineId, styleId });

if (query !== undefined) {
const accentPhrases = query.accentPhrases;
const newAccentPhrases: AccentPhrase[] = await dispatch(
"FETCH_MORA_DATA",
{
accentPhrases,
engineId,
styleId,
await dispatch("SETUP_SPEAKER", { audioKeys, engineId, styleId });
await Promise.all(
audioKeys.map(async (audioKey) => {
try {
const query = state.audioItems[audioKey].query;
if (query !== undefined) {
const accentPhrases = query.accentPhrases;
const newAccentPhrases: AccentPhrase[] = await dispatch(
"FETCH_MORA_DATA",
{
accentPhrases,
engineId,
styleId,
}
);
commit("COMMAND_MULTI_CHANGE_VOICE", {
audioKeys,
voice,
update: "AccentPhrases",
accentPhrases: newAccentPhrases,
});
} else {
const text = state.audioItems[audioKey].text;
const query: AudioQuery = await dispatch("FETCH_AUDIO_QUERY", {
text: text,
engineId,
styleId,
});
commit("COMMAND_MULTI_CHANGE_VOICE", {
audioKeys,
voice,
update: "AudioQuery",
query,
});
}
);
commit("COMMAND_CHANGE_VOICE", {
audioKey,
voice,
update: "AccentPhrases",
accentPhrases: newAccentPhrases,
});
} else {
const text = state.audioItems[audioKey].text;
const query: AudioQuery = await dispatch("FETCH_AUDIO_QUERY", {
text: text,
engineId,
styleId,
});
commit("COMMAND_CHANGE_VOICE", {
audioKey,
voice,
update: "AudioQuery",
query,
});
}
} catch (error) {
commit("COMMAND_CHANGE_VOICE", {
audioKey,
voice,
update: "RollbackStyleId",
});
throw error;
}
} catch (error) {
commit("COMMAND_MULTI_CHANGE_VOICE", {
audioKeys,
voice,
update: "RollbackStyleId",
});
throw error;
}
})
);
},
},

Expand Down
14 changes: 7 additions & 7 deletions src/store/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export type StoreType<T, U extends "getter" | "mutation" | "action"> = {
export type AudioStoreState = {
characterInfos: Record<EngineId, CharacterInfo[]>;
morphableTargetsInfo: Record<EngineId, MorphableTargetInfoTable>;
audioKeyInitializingSpeaker?: string;
audioKeysWithInitializingSpeaker: AudioKey[];
audioItems: Record<AudioKey, AudioItem>;
audioKeys: AudioKey[];
audioStates: Record<AudioKey, AudioState>;
Expand Down Expand Up @@ -186,14 +186,14 @@ export type AudioStoreTypes = {

SETUP_SPEAKER: {
action(payload: {
audioKey: AudioKey;
audioKeys: AudioKey[];
engineId: EngineId;
styleId: StyleId;
}): void;
};

SET_AUDIO_KEY_INITIALIZING_SPEAKER: {
mutation: { audioKey?: AudioKey };
SET_AUDIO_KEYS_WITH_INITIALIZING_SPEAKER: {
mutation: { audioKeys: AudioKey[] };
};

SET_ACTIVE_AUDIO_KEY: {
Expand Down Expand Up @@ -525,8 +525,8 @@ export type AudioCommandStoreTypes = {
action(payload: { audioKey: AudioKey; text: string }): void;
};

COMMAND_CHANGE_VOICE: {
mutation: { audioKey: AudioKey; voice: Voice } & (
COMMAND_MULTI_CHANGE_VOICE: {
mutation: { audioKeys: AudioKey[]; voice: Voice } & (
| { update: "RollbackStyleId" }
| {
update: "AccentPhrases";
Expand All @@ -537,7 +537,7 @@ export type AudioCommandStoreTypes = {
query: AudioQuery;
}
);
action(payload: { audioKey: AudioKey; voice: Voice }): void;
action(payload: { audioKeys: AudioKey[]; voice: Voice }): void;
};

COMMAND_CHANGE_ACCENT: {
Expand Down
10 changes: 10 additions & 0 deletions tests/e2e/browser/複数選択/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Page } from "@playwright/test";

export async function addAudioCells(page: Page, count: number) {
for (let i = 0; i < count; i++) {
await page.getByRole("button", { name: "テキストを追加" }).click();
await page.waitForTimeout(100);
}
}

export const ctrlLike = process.platform === "darwin" ? "Meta" : "Control";
62 changes: 62 additions & 0 deletions tests/e2e/browser/複数選択/値変更.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { test, expect, Page } from "@playwright/test";
import { toggleSetting, navigateToMain } from "../../navigators";
import { addAudioCells } from "./utils";

/*
* 全てのAudioCellのキャラクター+スタイル名を取得する。
* キャラクター+スタイル名はalt属性から取得する。
*
* @returns キャラクター名の配列
*/
async function getSelectedCharacters(page: Page): Promise<string[]> {
const characterNames = await page.evaluate(() => {
const audioCells = [...document.querySelectorAll(".audio-cell")];
const characterNames: string[] = [];
for (const audioCell of audioCells) {
const character = audioCell.querySelector(".icon-container > img");
if (character) {
const alt = character.getAttribute("alt");
if (!alt) {
throw new Error("alt属性がありません");
}

characterNames.push(alt);
}
}
return characterNames;
});
return characterNames;
}

test.beforeEach(async ({ page }) => {
const BASE_URL = "http://localhost:5173/#/home";
await page.setViewportSize({ width: 800, height: 600 });
await page.goto(BASE_URL);

await navigateToMain(page);
await page.waitForTimeout(100);
await toggleSetting(page, "複数選択");

await addAudioCells(page, 3);
});

test("複数選択:キャラクター選択", async ({ page }) => {
await page.locator(".audio-cell:nth-child(2)").click();
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowDown");
await page.keyboard.press("ArrowDown");
await page.keyboard.up("Shift");
await page.waitForTimeout(100);

await page.locator(".audio-cell:nth-child(2) .character-button").click();
await page.waitForTimeout(100);

await page.locator(".character-item-container .q-item:nth-child(2)").click();
await page.waitForTimeout(100);

const characterNames = await getSelectedCharacters(page);

expect(characterNames[0]).not.toEqual(characterNames[1]);
expect(characterNames[1]).toEqual(characterNames[2]);
expect(characterNames[1]).toEqual(characterNames[3]);
});
Loading