Skip to content
Merged
17 changes: 16 additions & 1 deletion src/components/Dialog/SettingDialog/SettingDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,12 @@
:modelValue="showTextLineNumber"
@update:modelValue="changeShowTextLineNumber"
/>
<ToggleCell
title="音声の長さの表示"
description="ONの場合、テキスト欄の右側に音声の長さが表示されます。"
:modelValue="showAudioLength"
@update:modelValue="changeShowAudioLength"
/>
<ToggleCell
title="テキスト追加ボタンの表示"
description="OFFの場合、右下にテキスト追加ボタンが表示されません。(テキスト欄は Shift + Enter で追加できます)"
Expand Down Expand Up @@ -406,7 +412,11 @@
v-for="(value, key) in undoableTrackOperations"
:key
:checked="value"
:label="undoableTrackOperationsLabels[key]"
:label="
undoableTrackOperationsLabels[
key as keyof typeof undoableTrackOperationsLabels
]
"
@update:checked="
(newValue) =>
(undoableTrackOperations = {
Expand Down Expand Up @@ -651,6 +661,11 @@ const [showTextLineNumber, changeShowTextLineNumber] = useRootMiscSetting(
"showTextLineNumber",
);

const [showAudioLength, changeShowAudioLength] = useRootMiscSetting(
store,
"showAudioLength",
);

const [_enableKatakanaEnglish, setEnableKatakanaEnglish] = useRootMiscSetting(
store,
"enableKatakanaEnglish",
Expand Down
30 changes: 29 additions & 1 deletion src/components/Talk/AudioCell.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,15 @@
文章が長いと正常に動作しない可能性があります。
句読点の位置で文章を分割してください。
</template>
<template v-if="enableDeleteButton" #after>
<template #after>
<div
v-if="showAudioLength && audioDuration !== undefined"
class="q-mr-sm audio-length"
>
{{ audioDuration.toFixed(2) }}s
</div>
<QBtn
v-if="enableDeleteButton"
round
flat
icon="delete_outline"
Expand Down Expand Up @@ -123,6 +130,7 @@ import {
useCommandOrControlKey,
} from "@/composables/useModifierKey";
import { getDefaultStyle } from "@/domain/talk";
import { calculateAudioLength } from "@/store/audioGenerate";

const props = defineProps<{
audioKey: AudioKey;
Expand Down Expand Up @@ -305,6 +313,18 @@ watch(
},
);

const showAudioLength = computed(() => store.state.showAudioLength);

const audioDuration = computed(() => {
if (!audioItem.value?.query) return undefined;
const engineId = audioItem.value.voice.engineId;
const supportedFeatures =
store.state.engineManifests[engineId]?.supportedFeatures;
if (!supportedFeatures?.adjustPhonemeLength) return undefined;

return calculateAudioLength(audioItem.value.query);
});

const pushAudioTextIfNeeded = async (event?: KeyboardEvent) => {
if (event && event.isComposing) return;
if (!willRemove.value && isChangeFlag.value && !willFocusOrBlur.value) {
Expand Down Expand Up @@ -751,4 +771,12 @@ const isMultipleEngine = computed(() => store.state.engineIds.length > 1);
z-index: 1;
cursor: default;
}

.audio-length {
color: colors.$display;
opacity: 0.6;
white-space: nowrap;
font-size: 0.85rem;
user-select: none;
}
</style>
24 changes: 24 additions & 0 deletions src/store/audioGenerate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,27 @@ export function handlePossiblyNotMorphableError(e: unknown) {
return;
}
}

/**
* AudioQueryから音声の長さを計算する(秒)
*/
export function calculateAudioLength(audioQuery: EditorAudioQuery) {
if (audioQuery.accentPhrases.length === 0) return 0;

let length = 0;
length += audioQuery.prePhonemeLength;
audioQuery.accentPhrases.forEach((accentPhrase) => {
accentPhrase.moras.forEach((mora) => {
if (mora.consonantLength != undefined) {
length += mora.consonantLength;
}
length += mora.vowelLength;
});
if (accentPhrase.pauseMora != undefined) {
length +=
accentPhrase.pauseMora.vowelLength * audioQuery.pauseLengthScale;
}
});
length += audioQuery.postPhonemeLength;
return length / audioQuery.speedScale;
}
2 changes: 2 additions & 0 deletions src/store/setting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const settingStoreState: SettingStoreState = {
playheadPositionDisplayFormat: "MINUTES_SECONDS",
enableKatakanaEnglish: true,
enableMultiSelect: true,
showAudioLength: false,
};

export const settingStore = createPartialStore<SettingStoreTypes>({
Expand Down Expand Up @@ -156,6 +157,7 @@ export const settingStore = createPartialStore<SettingStoreTypes>({
"openedEditor",
"enableKatakanaEnglish",
"enableMultiSelect",
"showAudioLength",
] as const;

// rootMiscSettingKeysに値を足し忘れていたときに型エラーを出す検出用コード
Expand Down
39 changes: 21 additions & 18 deletions src/type/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,6 @@ export type SplitTextWhenPasteType = "PERIOD_AND_NEW_LINE" | "NEW_LINE" | "OFF";

export type EditorFontType = "default" | "os";

export type SavingSetting = ConfigType["savingSetting"];

export type EngineSettings = Record<EngineId, EngineSettingType>;

export const engineSettingSchema = z.object({
useGpu: z.boolean().default(false),
outputSamplingRate: z
Expand All @@ -218,6 +214,25 @@ export const engineSettingSchema = z.object({
});
export type EngineSettingType = z.infer<typeof engineSettingSchema>;

export const savingSettingSchema = z
.object({
fileEncoding: z.enum(["UTF-8", "Shift_JIS"]).default("UTF-8"),
fileNamePattern: z.string().default(""), // NOTE: ファイル名パターンは拡張子を含まない
fixedExportEnabled: z.boolean().default(false),
avoidOverwrite: z.boolean().default(false),
fixedExportDir: z.string().default(""),
exportLab: z.boolean().default(false),
exportText: z.boolean().default(false),
outputStereo: z.boolean().default(false),
audioOutputDevice: z.string().default(""),
songTrackFileNamePattern: z.string().default(""),
})
.prefault({});

export type SavingSetting = z.infer<typeof savingSettingSchema>;

export type EngineSettings = Record<EngineId, EngineSettingType>;

export type DefaultStyleId = {
engineId: EngineId;
speakerUuid: SpeakerId;
Expand Down Expand Up @@ -405,6 +420,7 @@ export const rootMiscSettingSchema = z.object({
.default("MINUTES_SECONDS"), // 再生ヘッド位置の表示モード
enableKatakanaEnglish: z.boolean().default(true), // 未知の英単語をカタカナ読みに変換するかどうか
enableMultiSelect: z.boolean().default(true), // 複数選択を有効にするかどうか
showAudioLength: z.boolean().default(false), // 音声の長さを表示するかどうか
});
export type RootMiscSettingType = z.infer<typeof rootMiscSettingSchema>;

Expand All @@ -414,20 +430,7 @@ export function getConfigSchema({ isMac }: { isMac: boolean }) {
activePointScrollMode: z
.enum(["CONTINUOUSLY", "PAGE", "OFF"])
.default("OFF"),
savingSetting: z
.object({
fileEncoding: z.enum(["UTF-8", "Shift_JIS"]).default("UTF-8"),
fileNamePattern: z.string().default(""), // NOTE: ファイル名パターンは拡張子を含まない
fixedExportEnabled: z.boolean().default(false),
avoidOverwrite: z.boolean().default(false),
fixedExportDir: z.string().default(""),
exportLab: z.boolean().default(false),
exportText: z.boolean().default(false),
outputStereo: z.boolean().default(false),
audioOutputDevice: z.string().default(""),
songTrackFileNamePattern: z.string().default(""),
})
.prefault({}),
savingSetting: savingSettingSchema,
hotkeySettings: hotkeySettingSchema
.array()
.default(getDefaultHotkeySettings({ isMac })),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ test("スクリーンショット", async ({ page }) => {
await page.waitForTimeout(500);

// スクリーンショット撮影とスクロールを繰り返す
for (let i = 0; i < 5; i++) {
for (let i = 0; i < 6; i++) {
await expect(page).toHaveScreenshot(`スクリーンショット_${i}.png`);
await page.mouse.wheel(0, 500);
await page.waitForTimeout(300);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

171 changes: 171 additions & 0 deletions tests/unit/store/audioGenerate.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { describe, test, expect } from "vitest";
import { calculateAudioLength } from "@/store/audioGenerate";
import { EditorAudioQuery } from "@/store/type";

const baseEditorAudioQuery: EditorAudioQuery = {
accentPhrases: [],
speedScale: 1,
pitchScale: 0,
intonationScale: 1,
volumeScale: 1,
prePhonemeLength: 0.1,
postPhonemeLength: 0.1,
pauseLengthScale: 1,
outputSamplingRate: 24000,
outputStereo: false,
kana: "",
};

describe("audioGenerate", () => {
describe("calculateAudioLength", () => {
test("アクセント句がない場合は0を返すこと", () => {
const audioQuery: EditorAudioQuery = {
...baseEditorAudioQuery,
accentPhrases: [],
};

expect(calculateAudioLength(audioQuery)).toBe(0);
});

test("アクセント句がある場合の計算が正しいこと", () => {
const audioQuery: EditorAudioQuery = {
...baseEditorAudioQuery,
accentPhrases: [
{
moras: [
{
text: "あ",
vowel: "a",
vowelLength: 0.2,
pitch: 0,
consonant: "a",
consonantLength: 0.1,
}, // 0.3
{
text: "い",
vowel: "i",
vowelLength: 0.2,
pitch: 0,
consonant: undefined,
consonantLength: undefined,
}, // 0.2
],
accent: 1,
pauseMora: undefined,
},
],
};

// 0.1(pre) + 0.3 + 0.2 + 0.1(post) = 0.7
expect(calculateAudioLength(audioQuery)).toBeCloseTo(0.7);
});

test("speedScaleが反映されること", () => {
const audioQuery: EditorAudioQuery = {
...baseEditorAudioQuery,
accentPhrases: [
{
moras: [
{
text: "あ",
vowel: "a",
vowelLength: 0.2,
pitch: 0,
consonant: "a",
consonantLength: 0.1,
},
],
accent: 1,
pauseMora: undefined,
},
],
speedScale: 2, // 2倍速
};

// (0.1(pre) + 0.3 + 0.1(post)) / 2 = 0.25
expect(calculateAudioLength(audioQuery)).toBeCloseTo(0.25);
});

test("ポーズがある場合の計算が正しいこと", () => {
const audioQuery: EditorAudioQuery = {
...baseEditorAudioQuery,
accentPhrases: [
{
moras: [
{
text: "あ",
vowel: "a",
vowelLength: 0.2,
pitch: 0,
consonant: "a",
consonantLength: 0.1,
},
],
accent: 1,
pauseMora: {
text: "、",
vowel: "pau",
vowelLength: 0.5,
pitch: 0,
}, // 0.5 * 1.5 = 0.75
},
],
pauseLengthScale: 1.5,
};

// 0.1 + 0.3 + 0.75 + 0.1 = 1.25
expect(calculateAudioLength(audioQuery)).toBeCloseTo(1.25);
});
test("prePhonemeLengthが反映されること", () => {
const audioQuery: EditorAudioQuery = {
...baseEditorAudioQuery,
accentPhrases: [
{
moras: [
{
text: "あ",
vowel: "a",
vowelLength: 0.2,
pitch: 0,
consonant: "a",
consonantLength: 0.1,
},
],
accent: 1,
pauseMora: undefined,
},
],
prePhonemeLength: 0.5,
};

// 0.5(pre) + 0.3 + 0.1(post) = 0.9
expect(calculateAudioLength(audioQuery)).toBeCloseTo(0.9);
});

test("postPhonemeLengthが反映されること", () => {
const audioQuery: EditorAudioQuery = {
...baseEditorAudioQuery,
accentPhrases: [
{
moras: [
{
text: "あ",
vowel: "a",
vowelLength: 0.2,
pitch: 0,
consonant: "a",
consonantLength: 0.1,
},
],
accent: 1,
pauseMora: undefined,
},
],
postPhonemeLength: 0.5,
};

// 0.1(pre) + 0.3 + 0.5(post) = 0.9
expect(calculateAudioLength(audioQuery)).toBeCloseTo(0.9);
});
});
});
Loading