Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into エンジンのmockを作る
Browse files Browse the repository at this point in the history
  • Loading branch information
Hiroshiba committed Jul 6, 2024
2 parents b482e48 + cdb31d9 commit 0168fb7
Show file tree
Hide file tree
Showing 13 changed files with 719 additions and 229 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ npm ci

Windows の場合でもパスの区切り文字は`\`ではなく`/`なのでご注意ください。

また、macOS 向けの`VOICEVOX.app`を利用している場合は`/path/to/VOICEVOX.app/Contents/MacOS/run`を指定してください。
また、macOS 向けの`VOICEVOX.app`を利用している場合は`/path/to/VOICEVOX.app/Contents/MacOS/vv-engine/run`を指定してください。

Linux の場合は、[Releases](https://github.com/VOICEVOX/voicevox/releases/)から入手できる tar.gz 版に含まれる`run`コマンドを指定してください。
AppImage 版の場合は`$ /path/to/VOICEVOX.AppImage --appimage-mount`でファイルシステムをマウントできます。
Expand Down
679 changes: 541 additions & 138 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,10 @@
"@types/semver": "7.3.9",
"@types/wicg-file-system-access": "2020.9.6",
"@types/yargs": "17.0.32",
"@typescript-eslint/eslint-plugin": "7.11.0",
"@typescript-eslint/parser": "7.11.0",
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/utils": "7.11.0",
"@typescript-eslint/eslint-plugin": "7.15.0",
"@typescript-eslint/parser": "7.15.0",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/utils": "7.15.0",
"@vitejs/plugin-vue": "5.0.4",
"@voicevox/eslint-plugin": "file:./eslint-plugin",
"@vue/eslint-config-prettier": "9.0.0",
Expand All @@ -119,14 +119,14 @@
"sass": "1.32.13",
"storybook": "8.1.10",
"ts-node": "10.9.1",
"typescript": "5.4.5",
"vite": "5.2.9",
"vite-plugin-checker": "0.6.4",
"typescript": "5.5.2",
"vite": "5.3.2",
"vite-plugin-checker": "0.7.0",
"vite-plugin-electron": "0.28.4",
"vite-plugin-node-polyfills": "0.21.0",
"vite-tsconfig-paths": "4.2.1",
"vitest": "1.5.0",
"vue-tsc": "1.8.19",
"vue-tsc": "2.0.24",
"yargs": "17.2.1"
}
}
18 changes: 18 additions & 0 deletions src/backend/electron/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function ipcMainHandle(
...args: unknown[]
) => {
try {
validateIpcSender(event);
return listener(event, ...args);
} catch (e) {
log.error(e);
Expand All @@ -38,3 +39,20 @@ export function ipcMainSend(
): void {
return win.webContents.send(channel, ...args);
}

/** IPCメッセージの送信元を確認する */
const validateIpcSender = (event: IpcMainInvokeEvent) => {
let isValid: boolean;
const senderUrl = new URL(event.senderFrame.url);
if (process.env.VITE_DEV_SERVER_URL != undefined) {
const devServerUrl = new URL(process.env.VITE_DEV_SERVER_URL);
isValid = senderUrl.origin === devServerUrl.origin;
} else {
isValid = senderUrl.protocol === "app:";
}
if (!isValid) {
throw new Error(
`不正なURLからのIPCメッセージを検出しました。senderUrl: ${senderUrl.toString()}`,
);
}
};
37 changes: 32 additions & 5 deletions src/backend/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import path from "path";

import fs from "fs";
import { pathToFileURL } from "url";
import {
app,
protocol,
Expand Down Expand Up @@ -428,8 +429,23 @@ async function createWindow() {
// ソフトウェア起動時はプロトコルを app にする
if (process.env.VITE_DEV_SERVER_URL == undefined) {
protocol.handle("app", (request) => {
const filePath = path.join(__dirname, new URL(request.url).pathname);
return net.fetch(`file://${filePath}`);
// 読み取り先のファイルがインストールディレクトリ内であることを確認する
// ref: https://www.electronjs.org/ja/docs/latest/api/protocol#protocolhandlescheme-handler
const { pathname } = new URL(request.url);
const pathToServe = path.resolve(path.join(__dirname, pathname));
const relativePath = path.relative(__dirname, pathToServe);
const isUnsafe =
path.isAbsolute(relativePath) ||
relativePath.startsWith("..") ||
relativePath === "";
if (isUnsafe) {
log.error(`Bad Request URL: ${request.url}`);
return new Response("bad", {
status: 400,
headers: { "content-type": "text/html" },
});
}
return net.fetch(pathToFileURL(pathToServe).toString());
});
}

Expand Down Expand Up @@ -1018,11 +1034,22 @@ ipcMainHandle("READ_FILE", async (_, { filePath }) => {
app.on("web-contents-created", (e, contents) => {
// リンククリック時はブラウザを開く
contents.setWindowOpenHandler(({ url }) => {
if (url.match(/^http/)) {
const { protocol } = new URL(url);
if (protocol.match(/^https?:/)) {
shell.openExternal(url);
return { action: "deny" };
} else {
log.error(`許可されないリンクです。url: ${url}`);
}
return { action: "deny" };
});

// ナビゲーションを無効化
contents.on("will-navigate", (event) => {
// preloadスクリプト変更時のホットリロードを許容する
if (contents.getURL() !== event.url) {
log.error(`ナビゲーションは無効化されています。url: ${event.url}`);
event.preventDefault();
}
return { action: "allow" };
});
});

Expand Down
26 changes: 14 additions & 12 deletions src/components/Dialog/EngineManageDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -448,18 +448,20 @@ const getEngineTypeName = (name: string) => {
};
const getFeatureName = (name: keyof SupportedFeatures) => {
const featureNameMap: { [key in keyof SupportedFeatures]: string } = {
adjustMoraPitch: "モーラごとの音高の調整",
adjustPhonemeLength: "音素ごとの長さの調整",
adjustSpeedScale: "全体の話速の調整",
adjustPitchScale: "全体の音高の調整",
adjustIntonationScale: "全体の抑揚の調整",
adjustVolumeScale: "全体の音量の調整",
interrogativeUpspeak: "疑問文の自動調整",
synthesisMorphing: "2種類のスタイルでモーフィングした音声を合成",
sing: "歌唱音声合成",
manageLibrary: "音声ライブラリ(vvlib)の管理",
};
const featureNameMap: { [key in keyof Required<SupportedFeatures>]: string } =
{
adjustMoraPitch: "モーラごとの音高の調整",
adjustPhonemeLength: "音素ごとの長さの調整",
adjustSpeedScale: "全体の話速の調整",
adjustPitchScale: "全体の音高の調整",
adjustIntonationScale: "全体の抑揚の調整",
adjustVolumeScale: "全体の音量の調整",
interrogativeUpspeak: "疑問文の自動調整",
synthesisMorphing: "2種類のスタイルでモーフィングした音声を合成",
sing: "歌唱音声合成",
manageLibrary: "音声ライブラリのインストール・アンインストール",
returnResourceUrl: "キャラクター情報のリソースをURLで返送",
};
return featureNameMap[name];
};
Expand Down
24 changes: 24 additions & 0 deletions src/helpers/fileHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ResultError } from "@/type/result";

/** ファイル書き込み時のエラーメッセージを生成する */
export function generateWriteErrorMessage(writeFileResult: ResultError) {
const code = writeFileResult.code?.toUpperCase();

if (code?.startsWith("ENOSPC")) {
return "空き容量が足りません。";
}

if (code?.startsWith("EACCES")) {
return "ファイルにアクセスする許可がありません。";
}

if (code?.startsWith("EBUSY")) {
return "ファイルが開かれています。";
}

if (code?.startsWith("ENOENT")) {
return "ファイルが見つかりません。";
}

return `何らかの理由で失敗しました。${writeFileResult.message}`;
}
116 changes: 57 additions & 59 deletions src/store/audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
formatCharacterStyleName,
TuningTranscription,
filterCharacterInfosByStyleType,
DEFAULT_PROJECT_NAME,
} from "./utility";
import { createPartialStore } from "./vuex";
import { determineNextPresetKey } from "./preset";
Expand Down Expand Up @@ -56,6 +57,7 @@ import {
import { AudioQuery, AccentPhrase, Speaker, SpeakerInfo } from "@/openapi";
import { base64ImageToUri, base64ToUri } from "@/helpers/base64Helper";
import { getValueOrThrow, ResultError } from "@/type/result";
import { generateWriteErrorMessage } from "@/helpers/fileHelper";

function generateAudioKey() {
return AudioKey(crypto.randomUUID());
Expand Down Expand Up @@ -165,25 +167,6 @@ export async function writeTextFile(obj: {
});
}

function generateWriteErrorMessage(writeFileResult: ResultError) {
if (!writeFileResult.code) {
return `何らかの理由で失敗しました。${writeFileResult.message}`;
}
const code = writeFileResult.code.toUpperCase();

if (code.startsWith("ENOSPC")) {
return "空き容量が足りません。";
}

if (code.startsWith("EACCES")) {
return "ファイルにアクセスする許可がありません。";
}

if (code.startsWith("EBUSY")) {
return "ファイルが開かれています。";
}
}

// TODO: GETTERに移動する。
export function getCharacterInfo(
state: State,
Expand Down Expand Up @@ -1216,12 +1199,14 @@ export const audioStore = createPartialStore<AudioStoreTypes>({

const defaultFileBaseName = sanitizeFileName(headTailItemText);

return defaultFileBaseName === "" ? "Untitled" : defaultFileBaseName;
return defaultFileBaseName === ""
? DEFAULT_PROJECT_NAME
: defaultFileBaseName;
},
},

DEFAULT_AUDIO_FILE_NAME: {
getter: (state) => (audioKey) => {
getter: (state, getters) => (audioKey) => {
const fileNamePattern = state.savingSetting.fileNamePattern;

const index = state.audioKeys.indexOf(audioKey);
Expand All @@ -1241,12 +1226,14 @@ export const audioStore = createPartialStore<AudioStoreTypes>({
if (style == undefined) throw new Error("assert style != undefined");

const styleName = style.styleName || DEFAULT_STYLE_NAME;
const projectName = getters.PROJECT_NAME ?? DEFAULT_PROJECT_NAME;
return buildAudioFileNameFromRawData(fileNamePattern, {
characterName: character.metas.speakerName,
index,
styleName,
text: audioItem.text,
date: currentDateString(),
projectName,
});
},
},
Expand Down Expand Up @@ -1517,8 +1504,6 @@ export const audioStore = createPartialStore<AudioStoreTypes>({
const labs: string[] = [];
const texts: string[] = [];

let labOffset = 0;

const base64Encoder = (blob: Blob): Promise<string | undefined> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
Expand All @@ -1539,6 +1524,7 @@ export const audioStore = createPartialStore<AudioStoreTypes>({
const totalCount = state.audioKeys.length;
let finishedCount = 0;

let labOffset = 0;
for (const audioKey of state.audioKeys) {
let fetchAudioResult: FetchAudioResult;
try {
Expand All @@ -1560,21 +1546,21 @@ export const audioStore = createPartialStore<AudioStoreTypes>({
return { result: "WRITE_ERROR", path: filePath };
}
encodedBlobs.push(encodedBlob);

// 大して処理能力を要しないので、生成設定のon/offにかかわらず生成してしまう
const lab = await generateLabFromAudioQuery(audioQuery, labOffset);
if (lab == undefined) {
return { result: "WRITE_ERROR", path: filePath };
}
labs.push(lab);

// 最終音素の終了時刻を取得する
const splitLab = lab.split(" ");
labOffset = Number(splitLab[splitLab.length - 2]);

texts.push(
extractExportText(state.audioItems[audioKey].text, {
enableMemoNotation: state.enableMemoNotation,
enableRubyNotation: state.enableRubyNotation,
}),
);
// 最終音素の終了時刻を取得する
const splitLab = lab.split(" ");
labOffset = Number(splitLab[splitLab.length - 2]);
}

const connectedWav = await dispatch("CONNECT_AUDIO", {
Expand All @@ -1584,40 +1570,48 @@ export const audioStore = createPartialStore<AudioStoreTypes>({
return { result: "ENGINE_ERROR", path: filePath };
}

const writeFileResult = await window.backend.writeFile({
filePath,
buffer: await connectedWav.arrayBuffer(),
});
if (!writeFileResult.ok) {
window.backend.logError(writeFileResult.error);
return { result: "WRITE_ERROR", path: filePath };
}
try {
await window.backend
.writeFile({
filePath,
buffer: await connectedWav.arrayBuffer(),
})
.then(getValueOrThrow);

if (state.savingSetting.exportLab) {
const labResult = await writeTextFile({
// `generateLabFromAudioQuery`で生成される文字列はすべて改行で終わるので、追加で改行を挟む必要はない
text: labs.join(""),
filePath: filePath.replace(/\.wav$/, ".lab"),
});
if (!labResult.ok) {
window.backend.logError(labResult.error);
return { result: "WRITE_ERROR", path: filePath };
if (state.savingSetting.exportLab) {
await writeTextFile({
// `generateLabFromAudioQuery`で生成される文字列はすべて改行で終わるので、追加で改行を挟む必要はない
text: labs.join(""),
filePath: filePath.replace(/\.wav$/, ".lab"),
}).then(getValueOrThrow);
}
}

if (state.savingSetting.exportText) {
const textResult = await writeTextFile({
text: texts.join("\n"),
filePath: filePath.replace(/\.wav$/, ".txt"),
encoding: state.savingSetting.fileEncoding,
});
if (!textResult.ok) {
window.backend.logError(textResult.error);
return { result: "WRITE_ERROR", path: filePath };
if (state.savingSetting.exportText) {
await writeTextFile({
text: texts.join("\n"),
filePath: filePath.replace(/\.wav$/, ".txt"),
encoding: state.savingSetting.fileEncoding,
}).then(getValueOrThrow);
}
}

return { result: "SUCCESS", path: filePath };
return { result: "SUCCESS", path: filePath };
} catch (e) {
window.backend.logError(e);
if (e instanceof ResultError) {
return {
result: "WRITE_ERROR",
path: filePath,
errorMessage: generateWriteErrorMessage(e),
};
}
return {
result: "UNKNOWN_ERROR",
path: filePath,
errorMessage:
(e instanceof Error ? e.message : String(e)) ||
"不明なエラーが発生しました。",
};
}
},
),
},
Expand Down Expand Up @@ -1696,7 +1690,11 @@ export const audioStore = createPartialStore<AudioStoreTypes>({
});
if (!result.ok) {
window.backend.logError(result.error);
return { result: "WRITE_ERROR", path: filePath };
return {
result: "WRITE_ERROR",
path: filePath,
errorMessage: generateWriteErrorMessage(new ResultError(result)),
};
}

return { result: "SUCCESS", path: filePath };
Expand Down
Loading

0 comments on commit 0168fb7

Please sign in to comment.