From 7cc32cc3fb65225f21c02366c33618f3bac81318 Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Mon, 30 Dec 2024 07:50:01 +0900 Subject: [PATCH 01/23] =?UTF-8?q?test:=20VVPP=E3=83=87=E3=83=95=E3=82=A9?= =?UTF-8?q?=E3=83=AB=E3=83=88=E3=82=A8=E3=83=B3=E3=82=B8=E3=83=B3=E3=82=92?= =?UTF-8?q?=E4=BD=BF=E3=81=86=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E6=9B=B8?= =?UTF-8?q?=E3=81=8F=20(#2444)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.test-electron-default-vvpp | 17 ++++ .../electron/engineAndVvppController.ts | 2 +- src/domain/defaultEngine/envEngineInfo.ts | 7 +- tests/e2e/electron/example.spec.ts | 93 +++++++++++++------ vite.config.mts | 15 ++- 5 files changed, 100 insertions(+), 34 deletions(-) create mode 100644 .env.test-electron-default-vvpp diff --git a/.env.test-electron-default-vvpp b/.env.test-electron-default-vvpp new file mode 100644 index 0000000000..94255249d3 --- /dev/null +++ b/.env.test-electron-default-vvpp @@ -0,0 +1,17 @@ +# VVPPデフォルトエンジンでのテスト用の.envファイル。 + +VITE_APP_NAME=voicevox +VITE_DEFAULT_ENGINE_INFOS=`[ + { + "type": "downloadVvpp", + "name": "VOICEVOX Nemo Engine", + "uuid": "208cf94d-43d2-4cf5-abc0-9783cac36d29", + "executionEnabled": true, + "executionArgs": [], + "host": "http://127.0.0.1:50121", + "latestUrl": "https://voicevox.hiroshiba.jp/nemoLatestDefaultEngineInfos.json" + } +]` +VITE_OFFICIAL_WEBSITE_URL=https://voicevox.hiroshiba.jp/ +VITE_LATEST_UPDATE_INFOS_URL=https://voicevox.hiroshiba.jp/updateInfos.json +VITE_GTM_CONTAINER_ID=GTM-DUMMY diff --git a/src/backend/electron/engineAndVvppController.ts b/src/backend/electron/engineAndVvppController.ts index 5e0622c3ef..076a484570 100644 --- a/src/backend/electron/engineAndVvppController.ts +++ b/src/backend/electron/engineAndVvppController.ts @@ -50,11 +50,11 @@ export class EngineAndVvppController { await this.vvppManager.install(vvppPath); return true; } catch (e) { + log.error(`Failed to install ${vvppPath},`, e); dialog.showErrorBox( "インストールエラー", `${vvppPath} をインストールできませんでした。`, ); - log.error(`Failed to install ${vvppPath},`, e); return false; } } diff --git a/src/domain/defaultEngine/envEngineInfo.ts b/src/domain/defaultEngine/envEngineInfo.ts index f45098c264..4feee23d5c 100644 --- a/src/domain/defaultEngine/envEngineInfo.ts +++ b/src/domain/defaultEngine/envEngineInfo.ts @@ -5,6 +5,7 @@ import { z } from "zod"; import { engineIdSchema } from "@/type/preload"; +import { isElectron } from "@/helpers/platform"; /** .envに書くデフォルトエンジン情報のスキーマ */ const envEngineInfoSchema = z @@ -34,8 +35,12 @@ type EnvEngineInfoType = z.infer; /** .envからデフォルトエンジン情報を読み込む */ export function loadEnvEngineInfos(): EnvEngineInfoType[] { + // electronのときはプロセスの環境変数を参照する。 + // NOTE: electronテスト環境を切り替えるため。テスト環境が1本化されればimport.meta.envを使う。 const defaultEngineInfosEnv = - import.meta.env.VITE_DEFAULT_ENGINE_INFOS ?? "[]"; + (isElectron + ? process.env.VITE_DEFAULT_ENGINE_INFOS + : import.meta.env.VITE_DEFAULT_ENGINE_INFOS) ?? "[]"; // FIXME: 「.envを書き換えてください」というログを出したい // NOTE: domainディレクトリなのでログを出す方法がなく、Errorオプションのcauseを用いてもelectron-logがcauseのログを出してくれない diff --git a/tests/e2e/electron/example.spec.ts b/tests/e2e/electron/example.spec.ts index c2616c1f2c..d67508d9c0 100644 --- a/tests/e2e/electron/example.spec.ts +++ b/tests/e2e/electron/example.spec.ts @@ -3,10 +3,9 @@ import os from "os"; import path from "path"; import { _electron as electron, test } from "@playwright/test"; import dotenv from "dotenv"; +import { BrowserWindow, MessageBoxSyncOptions } from "electron"; test.beforeAll(async () => { - dotenv.config(); // FIXME: エンジンの設定直読み - console.log("Waiting for main.js to be built..."); while (true) { try { @@ -19,40 +18,80 @@ test.beforeAll(async () => { console.log("main.js is built."); }); -// キャッシュなどでテスト結果が変化しないように、appDataをテスト起動時に毎回消去する。 -// cf: https://www.electronjs.org/ja/docs/latest/api/app#appgetpathname -const appDataMap: Partial> = { - win32: process.env.APPDATA, - darwin: os.homedir() + "/Library/Application Support", - linux: process.env.XDG_CONFIG_HOME || os.homedir() + "/.config", -} as const; +test.beforeEach(async () => { + // キャッシュなどでテスト結果が変化しないように、appDataをテスト起動時に毎回消去する。 + // cf: https://www.electronjs.org/ja/docs/latest/api/app#appgetpathname + const appDataMap: Partial> = { + win32: process.env.APPDATA, + darwin: os.homedir() + "/Library/Application Support", + linux: process.env.XDG_CONFIG_HOME || os.homedir() + "/.config", + } as const; -const appData = appDataMap[process.platform]; -if (!appData) { - throw new Error("Unsupported platform"); -} -const userDir = path.resolve(appData, `${process.env.VITE_APP_NAME}-test`); + const appData = appDataMap[process.platform]; + if (!appData) { + throw new Error("Unsupported platform"); + } + const userDir = path.resolve(appData, `${process.env.VITE_APP_NAME}-test`); -test.beforeEach(async () => { await fs.rm(userDir, { recursive: true, force: true, }); }); -test("起動したら「利用規約に関するお知らせ」が表示される", async () => { - const app = await electron.launch({ - args: ["--no-sandbox", "."], // NOTE: --no-sandbox はUbuntu 24.04で動かすのに必要 - timeout: process.env.CI ? 0 : 60000, - }); +[ + { + envName: ".env環境", + envPath: ".env", + }, + { + envName: "VVPPデフォルトエンジン", + envPath: ".env.test-electron-default-vvpp", + }, +].forEach(({ envName, envPath }) => { + test.describe(`${envName}`, () => { + test.beforeEach(() => { + dotenv.config({ path: envPath, override: true }); + }); - const sut = await app.firstWindow({ - timeout: process.env.CI ? 60000 : 30000, - }); + test("起動したら「利用規約に関するお知らせ」が表示される", async () => { + const app = await electron.launch({ + args: ["--no-sandbox", "."], // NOTE: --no-sandbox はUbuntu 24.04で動かすのに必要 + timeout: process.env.CI ? 0 : 60000, + }); + + // ダイアログのモック + await app.evaluate((electron) => { + // @ts-expect-error 2種のオーバーロードを無視する + electron.dialog.showMessageBoxSync = ( + win: BrowserWindow, + options: MessageBoxSyncOptions, + ) => { + // デフォルトエンジンのインストールの確認ダイアログ + if ( + options.title == "デフォルトエンジンのインストール" && + options.buttons?.[0] == "インストール" + ) { + return 0; + } + + throw new Error(`Unexpected dialog: ${JSON.stringify(options)}`); + }; + }); + + // ログを表示 + app.on("console", (msg) => { + console.log(msg.text()); + }); - // エンジンが起動し「利用規約に関するお知らせ」が表示されるのを待つ - await sut.waitForSelector("text=利用規約に関するお知らせ", { - timeout: 60000, + const sut = await app.firstWindow({ + timeout: process.env.CI ? 60000 : 30000, + }); + // エンジンが起動し「利用規約に関するお知らせ」が表示されるのを待つ + await sut.waitForSelector("text=利用規約に関するお知らせ", { + timeout: 60000, + }); + await app.close(); + }); }); - await app.close(); }); diff --git a/vite.config.mts b/vite.config.mts index c3834795a7..e4da29c038 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -8,13 +8,18 @@ import vue from "@vitejs/plugin-vue"; import checker from "vite-plugin-checker"; import { BuildOptions, defineConfig, loadEnv, Plugin } from "vite"; import { quasar } from "@quasar/vite-plugin"; +import { z } from "zod"; const isElectron = process.env.VITE_TARGET === "electron"; const isBrowser = process.env.VITE_TARGET === "browser"; export default defineConfig((options) => { + const mode = z + .enum(["development", "test", "production"]) + .parse(options.mode); + const packageName = process.env.npm_package_name; - const env = loadEnv(options.mode, import.meta.dirname); + const env = loadEnv(mode, import.meta.dirname); if (!packageName?.startsWith(env.VITE_APP_NAME)) { throw new Error( `"package.json"の"name":"${packageName}"は"VITE_APP_NAME":"${env.VITE_APP_NAME}"から始まっている必要があります`, @@ -32,19 +37,19 @@ export default defineConfig((options) => { throw new Error(`Unsupported platform: ${process.platform}`); } process.env.VITE_7Z_BIN_NAME = - (options.mode === "development" + (mode !== "production" ? path.join(import.meta.dirname, "vendored", "7z") + path.sep : "") + sevenZipBinName; process.env.VITE_APP_VERSION = process.env.npm_package_version; - const shouldEmitSourcemap = ["development", "test"].includes(options.mode); + const shouldEmitSourcemap = ["development", "test"].includes(mode); const sourcemap: BuildOptions["sourcemap"] = shouldEmitSourcemap ? "inline" : false; // ref: electronの起動をスキップしてデバッグ起動を軽くする const skipLahnchElectron = - options.mode === "test" || process.env.SKIP_LAUNCH_ELECTRON === "1"; + mode === "test" || process.env.SKIP_LAUNCH_ELECTRON === "1"; return { root: path.resolve(import.meta.dirname, "src"), @@ -71,7 +76,7 @@ export default defineConfig((options) => { plugins: [ vue(), quasar({ autoImportComponentCase: "pascal" }), - options.mode !== "test" && + mode !== "test" && checker({ overlay: false, eslint: { From 9fd356b1a838daba9a8cb9d1d6a58964b81778a4 Mon Sep 17 00:00:00 2001 From: raa0121 Date: Tue, 31 Dec 2024 12:01:52 +0900 Subject: [PATCH 02/23] =?UTF-8?q?fix:=20=E3=83=97=E3=83=AD=E3=82=BB?= =?UTF-8?q?=E3=82=B9ID=E3=81=8B=E3=82=89=E3=83=97=E3=83=AD=E3=82=BB?= =?UTF-8?q?=E3=82=B9=E3=82=92=E7=89=B9=E5=AE=9A=E3=81=99=E3=82=8B=E3=81=AE?= =?UTF-8?q?=E3=82=92=20wmic=20=E3=81=8B=E3=82=89=20tasklist=20=E3=81=AB?= =?UTF-8?q?=E5=A4=89=E6=9B=B4=20(#2450)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nanashi. Co-authored-by: Hiroshiba --- .../electron/manager/engineProcessManager.ts | 2 +- src/backend/electron/portHelper.ts | 23 ++++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/backend/electron/manager/engineProcessManager.ts b/src/backend/electron/manager/engineProcessManager.ts index 2387b68da5..e02ec2fd30 100644 --- a/src/backend/electron/manager/engineProcessManager.ts +++ b/src/backend/electron/manager/engineProcessManager.ts @@ -99,7 +99,7 @@ export class EngineProcessManager { if (pid != undefined) { const processName = await getProcessNameFromPid(engineHostInfo, pid); log.warn( - `ENGINE ${engineId}: Port ${port} has already been assigned by ${processName} (pid=${pid})`, + `ENGINE ${engineId}: Port ${port} has already been assigned by ${processName ?? "(not found)"} (pid=${pid})`, ); } else { // ポートは使用不可能だがプロセスidは見つからなかった diff --git a/src/backend/electron/portHelper.ts b/src/backend/electron/portHelper.ts index cb68f9c15b..73fc05eefe 100644 --- a/src/backend/electron/portHelper.ts +++ b/src/backend/electron/portHelper.ts @@ -148,12 +148,12 @@ export async function getPidFromPort( export async function getProcessNameFromPid( hostInfo: HostInfo, pid: number, -): Promise { +): Promise { portLog(hostInfo.port, `Getting process name from pid=${pid}...`); const exec = isWindows ? { - cmd: "wmic", - args: ["process", "where", `"ProcessID=${pid}"`, "get", "name"], + cmd: "tasklist", + args: ["/FI", `"PID eq ${pid}"`, "/NH"], } : { cmd: "ps", @@ -165,15 +165,22 @@ export async function getProcessNameFromPid( /* * ex) stdout: * ``` - * Name - * node.exe + * + * node.exe 25180 Console 1 86,544 K * ``` * -> `node.exe` */ - const processName = isWindows ? stdout.split("\n")[1] : stdout; + const processName = ( + isWindows ? stdout.split("\r\n").at(1)?.split(/ +/)?.at(0) : stdout + )?.trim(); - portLog(hostInfo.port, `Found process name: ${processName}`); - return processName.trim(); + if (processName == undefined) { + portWarn(hostInfo.port, `Not found process name from pid=${pid}!`); + return undefined; + } else { + portLog(hostInfo.port, `Found process name: ${processName}`); + return processName; + } } /** From bd33fea86a33d26b67eabd634ba09902453e85f4 Mon Sep 17 00:00:00 2001 From: "Nanashi." Date: Wed, 1 Jan 2025 01:00:27 +0900 Subject: [PATCH 03/23] =?UTF-8?q?feat:=20=E3=83=97=E3=83=AD=E3=82=B8?= =?UTF-8?q?=E3=82=A7=E3=82=AF=E3=83=88=E3=81=AE=E3=82=A8=E3=82=AF=E3=82=B9?= =?UTF-8?q?=E3=83=9D=E3=83=BC=E3=83=88=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=20(#2428)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Hiroshiba Co-authored-by: Hiroshiba Kazuyuki --- src/backend/browser/fakePath.ts | 16 ++++ src/backend/browser/fileImpl.ts | 118 +++++++++++++++++++++------ src/backend/browser/sandbox.ts | 52 ++++++------ src/backend/electron/main.ts | 44 ++++------ src/backend/electron/preload.ts | 20 ++--- src/components/Dialog/Dialog.ts | 3 +- src/components/Sing/menuBarData.ts | 43 +++++++++- src/sing/utaformatixProject/utils.ts | 56 +++++++++++++ src/store/audio.ts | 12 ++- src/store/singing.ts | 107 +++++++++++++++++++++++- src/store/type.ts | 16 ++++ src/type/ipc.ts | 27 +++--- src/type/preload.ts | 14 ++-- 13 files changed, 404 insertions(+), 124 deletions(-) create mode 100644 src/backend/browser/fakePath.ts create mode 100644 src/sing/utaformatixProject/utils.ts diff --git a/src/backend/browser/fakePath.ts b/src/backend/browser/fakePath.ts new file mode 100644 index 0000000000..4c3d42074e --- /dev/null +++ b/src/backend/browser/fakePath.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; +import { uuid4 } from "@/helpers/random"; + +const fakePathSchema = z + .string() + .regex(/^-.+$/) + .brand("FakePath"); +export type FakePath = z.infer; + +export const isFakePath = (path: string): path is FakePath => { + return fakePathSchema.safeParse(path).success; +}; + +export const createFakePath = (name: string): FakePath => { + return fakePathSchema.parse(`-${name}`); +}; diff --git a/src/backend/browser/fileImpl.ts b/src/backend/browser/fileImpl.ts index 2c78966774..fcfc18742b 100644 --- a/src/backend/browser/fileImpl.ts +++ b/src/backend/browser/fileImpl.ts @@ -1,11 +1,12 @@ import { directoryHandleStoreKey } from "./contract"; import { openDB } from "./browserConfig"; +import { createFakePath, FakePath, isFakePath } from "./fakePath"; import { SandboxKey } from "@/type/preload"; import { failure, success } from "@/type/result"; import { createLogger } from "@/domain/frontend/log"; -import { uuid4 } from "@/helpers/random"; import { normalizeError } from "@/helpers/normalizeError"; import path from "@/helpers/path"; +import { ExhaustiveError } from "@/type/utility"; const log = createLogger("fileImpl"); @@ -113,17 +114,48 @@ const getDirectoryHandleFromDirectoryPath = async ( } }; +export type WritableFilePath = + | { + // ファイル名のみ。ダウンロードとして扱われます。 + type: "nameOnly"; + path: string; + } + | { + // ディレクトリ内への書き込み。 + type: "child"; + path: string; + } + | { + // 疑似パス。 + type: "fake"; + path: FakePath; + }; + // NOTE: fixedExportEnabled が有効になっている GENERATE_AND_SAVE_AUDIO action では、ファイル名に加えディレクトリ名も指定された状態でfilePathが渡ってくる // また GENERATE_AND_SAVE_ALL_AUDIO action では fixedExportEnabled の有効の有無に関わらず、ディレクトリ名も指定された状態でfilePathが渡ってくる -export const writeFileImpl: (typeof window)[typeof SandboxKey]["writeFile"] = - async (obj: { filePath: string; buffer: ArrayBuffer }) => { - const filePath = obj.filePath; +// showExportFilePicker での疑似パスが渡ってくる可能性もある。 +export const writeFileImpl = async (obj: { + filePath: WritableFilePath; + buffer: ArrayBuffer; +}) => { + const filePath = obj.filePath; - if (!filePath.includes(path.SEPARATOR)) { + switch (filePath.type) { + case "fake": { + const fileHandle = fileHandleMap.get(filePath.path); + if (fileHandle == undefined) { + return failure(new Error(`ファイルが見つかりません: ${filePath.path}`)); + } + const writable = await fileHandle.createWritable(); + await writable.write(obj.buffer); + return writable.close().then(() => success(undefined)); + } + + case "nameOnly": { const aTag = document.createElement("a"); const blob = URL.createObjectURL(new Blob([obj.buffer])); aTag.href = blob; - aTag.download = filePath; + aTag.download = filePath.path; document.body.appendChild(aTag); aTag.click(); document.body.removeChild(aTag); @@ -131,27 +163,32 @@ export const writeFileImpl: (typeof window)[typeof SandboxKey]["writeFile"] = return success(undefined); } - const fileName = resolveFileName(filePath); - const maybeDirectoryHandleName = resolveDirectoryName(filePath); + case "child": { + const fileName = resolveFileName(filePath.path); + const maybeDirectoryHandleName = resolveDirectoryName(filePath.path); - const directoryHandle = await getDirectoryHandleFromDirectoryPath( - maybeDirectoryHandleName, - ); + const directoryHandle = await getDirectoryHandleFromDirectoryPath( + maybeDirectoryHandleName, + ); - directoryHandleMap.set(maybeDirectoryHandleName, directoryHandle); + directoryHandleMap.set(maybeDirectoryHandleName, directoryHandle); - return directoryHandle - .getFileHandle(fileName, { create: true }) - .then(async (fileHandle) => { - const writable = await fileHandle.createWritable(); - await writable.write(obj.buffer); - return writable.close(); - }) - .then(() => success(undefined)) - .catch((e) => { - return failure(normalizeError(e)); - }); - }; + return directoryHandle + .getFileHandle(fileName, { create: true }) + .then(async (fileHandle) => { + const writable = await fileHandle.createWritable(); + await writable.write(obj.buffer); + return writable.close(); + }) + .then(() => success(undefined)) + .catch((e) => { + return failure(normalizeError(e)); + }); + } + default: + throw new ExhaustiveError(filePath); + } +}; export const checkFileExistsImpl: (typeof window)[typeof SandboxKey]["checkFileExists"] = async (filePath) => { @@ -182,7 +219,7 @@ export const checkFileExistsImpl: (typeof window)[typeof SandboxKey]["checkFileE }; // FileSystemFileHandleを保持するMap。キーは生成した疑似パス。 -const fileHandleMap: Map = new Map(); +const fileHandleMap: Map = new Map(); // ファイル選択ダイアログを開く // 返り値はファイルパスではなく、疑似パスを返す @@ -201,7 +238,7 @@ export const showOpenFilePickerImpl = async (options: { }); const paths = []; for (const handle of handles) { - const fakePath = `-${handle.name}`; + const fakePath = createFakePath(handle.name); fileHandleMap.set(fakePath, handle); paths.push(fakePath); } @@ -214,6 +251,9 @@ export const showOpenFilePickerImpl = async (options: { // 指定した疑似パスのファイルを読み込む export const readFileImpl = async (filePath: string) => { + if (!isFakePath(filePath)) { + return failure(new Error(`疑似パスではありません: ${filePath}`)); + } const fileHandle = fileHandleMap.get(filePath); if (fileHandle == undefined) { return failure(new Error(`ファイルが見つかりません: ${filePath}`)); @@ -222,3 +262,29 @@ export const readFileImpl = async (filePath: string) => { const buffer = await file.arrayBuffer(); return success(buffer); }; + +// ファイル選択ダイアログを開く +// 返り値はファイルパスではなく、疑似パスを返す +export const showExportFilePickerImpl: (typeof window)[typeof SandboxKey]["showExportFileDialog"] = + async (obj: { + defaultPath?: string; + extensionName: string; + extensions: string[]; + title: string; + }) => { + const handle = await showSaveFilePicker({ + suggestedName: obj.defaultPath, + types: [ + { + description: obj.extensions.join("、"), + accept: { + "application/octet-stream": obj.extensions.map((ext) => `.${ext}`), + }, + }, + ], + }); + const fakePath = createFakePath(handle.name); + fileHandleMap.set(fakePath, handle); + + return fakePath; + }; diff --git a/src/backend/browser/sandbox.ts b/src/backend/browser/sandbox.ts index 38b0f12e7a..de3279fb63 100644 --- a/src/backend/browser/sandbox.ts +++ b/src/backend/browser/sandbox.ts @@ -2,11 +2,14 @@ import { defaultEngine } from "./contract"; import { checkFileExistsImpl, readFileImpl, + showExportFilePickerImpl, showOpenDirectoryDialogImpl, showOpenFilePickerImpl, + WritableFilePath, writeFileImpl, } from "./fileImpl"; import { getConfigManager } from "./browserConfig"; +import { isFakePath } from "./fakePath"; import { IpcSOData } from "@/type/ipc"; import { defaultToolbarButtonSetting, @@ -17,6 +20,7 @@ import { } from "@/type/preload"; import { AssetTextFileNames } from "@/type/staticResources"; import { HotkeySettingType } from "@/domain/hotkeyAction"; +import path from "@/helpers/path"; const toStaticPath = (fileName: string) => `${import.meta.env.BASE_URL}/${fileName}`.replaceAll(/\/\/+/g, "/"); @@ -72,34 +76,6 @@ export const api: Sandbox = { // NOTE: ブラウザ版ではサポートされていません return Promise.resolve({}); }, - showAudioSaveDialog(obj: { title: string; defaultPath?: string }) { - return new Promise((resolve, reject) => { - if (obj.defaultPath == undefined) { - reject( - // storeやvue componentからdefaultPathを設定していなかったらrejectされる - new Error( - "ブラウザ版ではファイルの保存機能が一部サポートされていません。", - ), - ); - } else { - resolve(obj.defaultPath); - } - }); - }, - showTextSaveDialog(obj: { title: string; defaultPath?: string }) { - return new Promise((resolve, reject) => { - if (obj.defaultPath == undefined) { - reject( - // storeやvue componentからdefaultPathを設定していなかったらrejectされる - new Error( - "ブラウザ版ではファイルの保存機能が一部サポートされていません。", - ), - ); - } else { - resolve(obj.defaultPath); - } - }); - }, showSaveDirectoryDialog(obj: { title: string }) { return showOpenDirectoryDialogImpl(obj); }, @@ -163,8 +139,26 @@ export const api: Sandbox = { }); return fileHandle?.[0]; }, + async showExportFileDialog(obj: { + defaultPath?: string; + extensionName: string; + extensions: string[]; + title: string; + }) { + const fileHandle = await showExportFilePickerImpl(obj); + return fileHandle; + }, writeFile(obj: { filePath: string; buffer: ArrayBuffer }) { - return writeFileImpl(obj); + let filePath: WritableFilePath; + if (isFakePath(obj.filePath)) { + filePath = { type: "fake", path: obj.filePath }; + } else if (obj.filePath.includes(path.SEPARATOR)) { + filePath = { type: "child", path: obj.filePath }; + } else { + filePath = { type: "nameOnly", path: obj.filePath }; + } + + return writeFileImpl({ filePath, buffer: obj.buffer }); }, readFile(obj: { filePath: string }) { return readFileImpl(obj.filePath); diff --git a/src/backend/electron/main.ts b/src/backend/electron/main.ts index 2a810609b8..be092f8a26 100644 --- a/src/backend/electron/main.ts +++ b/src/backend/electron/main.ts @@ -469,35 +469,6 @@ registerIpcMainHandle({ return engineInfoManager.altPortInfos; }, - SHOW_AUDIO_SAVE_DIALOG: async (_, { title, defaultPath }) => { - const result = await retryShowSaveDialogWhileSafeDir(() => - dialog.showSaveDialog(win, { - title, - defaultPath, - filters: [ - { - name: "WAVファイル", - extensions: ["wav"], - }, - ], - properties: ["createDirectory"], - }), - ); - return result.filePath; - }, - - SHOW_TEXT_SAVE_DIALOG: async (_, { title, defaultPath }) => { - const result = await retryShowSaveDialogWhileSafeDir(() => - dialog.showSaveDialog(win, { - title, - defaultPath, - filters: [{ name: "Text File", extensions: ["txt"] }], - properties: ["createDirectory"], - }), - ); - return result.filePath; - }, - /** * 保存先になるディレクトリを選ぶダイアログを表示する。 */ @@ -600,6 +571,21 @@ registerIpcMainHandle({ })?.[0]; }, + SHOW_EXPORT_FILE_DIALOG: async ( + _, + { title, defaultPath, extensionName, extensions }, + ) => { + const result = await retryShowSaveDialogWhileSafeDir(() => + dialog.showSaveDialog(win, { + title, + defaultPath, + filters: [{ name: extensionName, extensions: extensions }], + properties: ["createDirectory"], + }), + ); + return result.filePath; + }, + IS_AVAILABLE_GPU_MODE: () => { return hasSupportedGpu(process.platform); }, diff --git a/src/backend/electron/preload.ts b/src/backend/electron/preload.ts index 2d3015b124..878c8f6448 100644 --- a/src/backend/electron/preload.ts +++ b/src/backend/electron/preload.ts @@ -33,17 +33,6 @@ const api: Sandbox = { return await ipcRendererInvokeProxy.GET_ALT_PORT_INFOS(); }, - showAudioSaveDialog: ({ title, defaultPath }) => { - return ipcRendererInvokeProxy.SHOW_AUDIO_SAVE_DIALOG({ - title, - defaultPath, - }); - }, - - showTextSaveDialog: ({ title, defaultPath }) => { - return ipcRendererInvokeProxy.SHOW_TEXT_SAVE_DIALOG({ title, defaultPath }); - }, - showSaveDirectoryDialog: ({ title }) => { return ipcRendererInvokeProxy.SHOW_SAVE_DIRECTORY_DIALOG({ title }); }, @@ -75,6 +64,15 @@ const api: Sandbox = { }); }, + showExportFileDialog: ({ title, defaultPath, extensionName, extensions }) => { + return ipcRendererInvokeProxy.SHOW_EXPORT_FILE_DIALOG({ + title, + defaultPath, + extensionName, + extensions, + }); + }, + writeFile: async ({ filePath, buffer }) => { return await ipcRendererInvokeProxy.WRITE_FILE({ filePath, buffer }); }, diff --git a/src/components/Dialog/Dialog.ts b/src/components/Dialog/Dialog.ts index a5aa17a655..38edc9b31f 100644 --- a/src/components/Dialog/Dialog.ts +++ b/src/components/Dialog/Dialog.ts @@ -13,7 +13,7 @@ import { import { DotNotationDispatch } from "@/store/vuex"; import { withProgress } from "@/store/ui"; -type MediaType = "audio" | "text" | "label"; +type MediaType = "audio" | "text" | "project" | "label"; export type TextDialogResult = "OK" | "CANCEL"; export type MessageDialogOptions = { @@ -302,6 +302,7 @@ const showWriteSuccessNotify = ({ const mediaTypeNames: Record = { audio: "音声", text: "テキスト", + project: "プロジェクト", label: "labファイル", }; void actions.SHOW_NOTIFY_AND_NOT_SHOW_AGAIN_BUTTON({ diff --git a/src/components/Sing/menuBarData.ts b/src/components/Sing/menuBarData.ts index 3ddeaaee4e..6f43491f5a 100644 --- a/src/components/Sing/menuBarData.ts +++ b/src/components/Sing/menuBarData.ts @@ -2,6 +2,7 @@ import { computed } from "vue"; import { useStore } from "@/store"; import { MenuItemData } from "@/components/Menu/type"; import { useRootMiscSetting } from "@/composables/useRootMiscSetting"; +import { ExportSongProjectFileType } from "@/store/type"; import { notifyResult } from "@/components/Dialog/Dialog"; export const useMenuBarData = () => { @@ -25,6 +26,23 @@ export const useMenuBarData = () => { }); }; + const exportSongProject = async ( + fileType: ExportSongProjectFileType, + fileTypeLabel: string, + ) => { + if (uiLocked.value) return; + const result = await store.actions.EXPORT_SONG_PROJECT({ + fileType, + fileTypeLabel, + }); + notifyResult( + result, + "project", + store.actions, + store.state.confirmedTips.notifyOnGenerate, + ); + }; + const exportLabelFile = async () => { const results = await store.actions.EXPORT_LABEL_FILES({}); @@ -60,12 +78,35 @@ export const useMenuBarData = () => { { type: "separator" }, { type: "button", - label: "インポート", + label: "プロジェクトをインポート", onClick: () => { void importExternalSongProject(); }, disableWhenUiLocked: true, }, + { + type: "root", + label: "プロジェクトをエクスポート", + subMenu: ( + [ + ["smf", "MIDI (SMF)"], + ["musicxml", "MusicXML"], + ["ufdata", "Utaformatix"], + ["ust", "UTAU"], + ] satisfies [fileType: ExportSongProjectFileType, label: string][] + ).map( + ([fileType, label]) => + ({ + type: "button", + label, + onClick: () => { + void exportSongProject(fileType, label); + }, + disableWhenUiLocked: true, + }) satisfies MenuItemData, + ), + disableWhenUiLocked: true, + }, ]); // 「編集」メニュー diff --git a/src/sing/utaformatixProject/utils.ts b/src/sing/utaformatixProject/utils.ts new file mode 100644 index 0000000000..dec64e1248 --- /dev/null +++ b/src/sing/utaformatixProject/utils.ts @@ -0,0 +1,56 @@ +import { Project as UfProject } from "@sevenc-nanashi/utaformatix-ts"; +import { ExhaustiveError } from "@/type/utility"; + +export const singleFileProjectFormats = ["smf", "ufdata"] as const; +export type SingleFileProjectFormat = (typeof singleFileProjectFormats)[number]; +export const multiFileProjectFormats = ["ust", "musicxml"] as const; +export type MultiFileProjectFormat = (typeof multiFileProjectFormats)[number]; +export type ProjectFormat = SingleFileProjectFormat | MultiFileProjectFormat; + +export const isSingleFileProjectFormat = ( + format: ProjectFormat, +): format is SingleFileProjectFormat => + singleFileProjectFormats.includes(format as SingleFileProjectFormat); + +export const isMultiFileProjectFormat = ( + format: ProjectFormat, +): format is MultiFileProjectFormat => + multiFileProjectFormats.includes(format as MultiFileProjectFormat); + +export const projectFileExtensions: Record< + SingleFileProjectFormat | MultiFileProjectFormat, + string +> = { + smf: "mid", + ufdata: "ufdata", + ust: "ust", + musicxml: "xml", +}; + +export const ufProjectToSingleFile = async ( + project: UfProject, + format: SingleFileProjectFormat, +): Promise => { + switch (format) { + case "smf": + return await project.toStandardMid(); + case "ufdata": + return await project.toUfData(); + default: + throw new ExhaustiveError(format); + } +}; + +export const ufProjectToMultiFile = async ( + project: UfProject, + format: MultiFileProjectFormat, +): Promise => { + switch (format) { + case "ust": + return await project.toUst(); + case "musicxml": + return await project.toMusicXml(); + default: + throw new ExhaustiveError(format); + } +}; diff --git a/src/store/audio.ts b/src/store/audio.ts index bea3d9425f..d433b55ef6 100644 --- a/src/store/audio.ts +++ b/src/store/audio.ts @@ -1370,9 +1370,11 @@ export const audioStore = createPartialStore({ defaultAudioFileName, ); } else { - filePath ??= await window.backend.showAudioSaveDialog({ + filePath ??= await window.backend.showExportFileDialog({ title: "音声を保存", defaultPath: defaultAudioFileName, + extensionName: "WAV ファイル", + extensions: ["wav"], }); } @@ -1517,9 +1519,11 @@ export const audioStore = createPartialStore({ defaultFileName, ); } else { - filePath ??= await window.backend.showAudioSaveDialog({ + filePath ??= await window.backend.showExportFileDialog({ title: "音声を全て繋げて保存", defaultPath: defaultFileName, + extensionName: "WAV ファイル", + extensions: ["wav"], }); } @@ -1660,9 +1664,11 @@ export const audioStore = createPartialStore({ defaultFileName, ); } else { - filePath ??= await window.backend.showTextSaveDialog({ + filePath ??= await window.backend.showExportFileDialog({ title: "文章を全て繋げてテキストファイルに保存", defaultPath: defaultFileName, + extensionName: "テキストファイル", + extensions: ["txt"], }); } diff --git a/src/store/singing.ts b/src/store/singing.ts index 65da791f3e..3e18f02736 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -114,7 +114,15 @@ import { generateWriteErrorMessage } from "@/helpers/fileHelper"; import { generateWavFileData } from "@/helpers/fileDataGenerator"; import path from "@/helpers/path"; import { showAlertDialog } from "@/components/Dialog/Dialog"; +import { ufProjectFromVoicevox } from "@/sing/utaformatixProject/fromVoicevox"; import { generateUniqueFilePath } from "@/sing/fileUtils"; +import { + isMultiFileProjectFormat, + isSingleFileProjectFormat, + projectFileExtensions, + ufProjectToMultiFile, + ufProjectToSingleFile, +} from "@/sing/utaformatixProject/utils"; const logger = createLogger("store/singing"); @@ -2760,9 +2768,11 @@ export const singingStore = createPartialStore({ if (state.savingSetting.fixedExportEnabled) { filePath = path.join(state.savingSetting.fixedExportDir, fileName); } else { - filePath ??= await window.backend.showAudioSaveDialog({ + filePath ??= await window.backend.showExportFileDialog({ title: "音声を保存", defaultPath: fileName, + extensions: ["wav"], + extensionName: "WAV ファイル", }); } if (!filePath) { @@ -3449,6 +3459,101 @@ export const singingStore = createPartialStore({ return Math.max(1, lastNoteEndTime + 1); }, }, + + EXPORT_SONG_PROJECT: { + action: createUILockAction( + async ( + { state, getters, actions }, + { fileType, fileTypeLabel }, + ): Promise => { + const fileBaseName = generateDefaultSongFileBaseName( + getters.PROJECT_NAME, + getters.SELECTED_TRACK, + getters.CHARACTER_INFO, + ); + const project = ufProjectFromVoicevox( + { + tempos: state.tempos, + timeSignatures: state.timeSignatures, + tpqn: state.tpqn, + tracks: state.trackOrder.map((trackId) => + getOrThrow(state.tracks, trackId), + ), + }, + fileBaseName, + ); + + // 複数トラックかつ複数ファイルの形式はディレクトリに書き出す + if (state.trackOrder.length > 1 && isMultiFileProjectFormat(fileType)) { + const dirPath = await window.backend.showSaveDirectoryDialog({ + title: "プロジェクトを書き出し", + }); + if (!dirPath) { + return { result: "CANCELED", path: "" }; + } + + const extension = projectFileExtensions[fileType]; + const tracksBytes = await ufProjectToMultiFile(project, fileType); + + let firstFilePath; + for (const [i, trackBytes] of tracksBytes.entries()) { + const filePath = await actions.GENERATE_FILE_PATH_FOR_TRACK_EXPORT({ + trackId: state.trackOrder[i], + directoryPath: dirPath, + extension, + }); + if (i === 0) { + firstFilePath = filePath; + } + + const result = await actions.EXPORT_FILE({ + filePath, + content: trackBytes, + }); + if (result.result !== "SUCCESS") { + return result; + } + } + if (firstFilePath == undefined) { + throw new Error("firstFilePath is undefined."); + } + + return { result: "SUCCESS", path: firstFilePath }; + } + + // それ以外の場合は単一ファイルの形式を選択する + else { + let buffer: Uint8Array; + const extension = projectFileExtensions[fileType]; + if (isSingleFileProjectFormat(fileType)) { + buffer = await ufProjectToSingleFile(project, fileType); + } else { + buffer = (await ufProjectToMultiFile(project, fileType))[0]; + } + + let filePath = await window.backend.showExportFileDialog({ + title: "プロジェクトを書き出し", + defaultPath: fileBaseName, + extensionName: fileTypeLabel, + extensions: [extension], + }); + if (!filePath) { + return { result: "CANCELED", path: "" }; + } + filePath = await generateUniqueFilePath( + // 拡張子を除いたファイル名を取得 + filePath.slice(0, -(extension.length + 1)), + extension, + ); + + return await actions.EXPORT_FILE({ + filePath, + content: buffer, + }); + } + }, + ), + }, }); export const singingCommandStoreState: SingingCommandStoreState = {}; diff --git a/src/store/type.ts b/src/store/type.ts index 37a9d9d36c..53c1081107 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -72,6 +72,10 @@ import { trackSchema, } from "@/domain/project/schema"; import { HotkeySettingType } from "@/domain/hotkeyAction"; +import { + MultiFileProjectFormat, + SingleFileProjectFormat, +} from "@/sing/utaformatixProject/utils"; /** * エディタ用のAudioQuery @@ -847,6 +851,11 @@ export type NoteEditTool = "SELECT_FIRST" | "EDIT_FIRST"; // ピッチ編集ツール export type PitchEditTool = "DRAW" | "ERASE"; +// プロジェクトの書き出しに使えるファイル形式 +export type ExportSongProjectFileType = + | SingleFileProjectFormat + | MultiFileProjectFormat; + export type TrackParameters = { gain: boolean; pan: boolean; @@ -1371,6 +1380,13 @@ export type SingingStoreTypes = { APPLY_DEVICE_ID_TO_AUDIO_CONTEXT: { action(payload: { device: string }): void; }; + + EXPORT_SONG_PROJECT: { + action(payload: { + fileType: ExportSongProjectFileType; + fileTypeLabel: string; + }): Promise; + }; }; export type SingingCommandStoreState = { diff --git a/src/type/ipc.ts b/src/type/ipc.ts index d71685f5ac..3414f186fd 100644 --- a/src/type/ipc.ts +++ b/src/type/ipc.ts @@ -33,21 +33,6 @@ export type IpcIHData = { return: AltPortInfos; }; - SHOW_AUDIO_SAVE_DIALOG: { - args: [ - obj: { - title: string; - defaultPath?: string; - }, - ]; - return?: string; - }; - - SHOW_TEXT_SAVE_DIALOG: { - args: [obj: { title: string; defaultPath?: string }]; - return?: string; - }; - SHOW_SAVE_DIRECTORY_DIALOG: { args: [obj: { title: string }]; return?: string; @@ -98,6 +83,18 @@ export type IpcIHData = { return: MessageBoxReturnValue; }; + SHOW_EXPORT_FILE_DIALOG: { + args: [ + obj: { + title: string; + defaultPath?: string; + extensionName: string; + extensions: string[]; + }, + ]; + return?: string; + }; + IS_AVAILABLE_GPU_MODE: { args: []; return: boolean; diff --git a/src/type/preload.ts b/src/type/preload.ts index cd1e6cf9e4..24c8f4d6db 100644 --- a/src/type/preload.ts +++ b/src/type/preload.ts @@ -73,14 +73,6 @@ export interface Sandbox { getAppInfos(): Promise; getTextAsset(textType: K): Promise; getAltPortInfos(): Promise; - showAudioSaveDialog(obj: { - title: string; - defaultPath?: string; - }): Promise; - showTextSaveDialog(obj: { - title: string; - defaultPath?: string; - }): Promise; showSaveDirectoryDialog(obj: { title: string }): Promise; showVvppOpenDialog(obj: { title: string; @@ -97,6 +89,12 @@ export interface Sandbox { name?: string; extensions?: string[]; }): Promise; + showExportFileDialog(obj: { + title: string; + defaultPath?: string; + extensionName: string; + extensions: string[]; + }): Promise; writeFile(obj: { filePath: string; buffer: ArrayBuffer | Uint8Array; From a52114a5e0bba2917662a49b80659bf413d97ca9 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Wed, 1 Jan 2025 21:01:09 +0900 Subject: [PATCH 04/23] =?UTF-8?q?fix:=20`writeFileSafely`=E3=81=AE?= =?UTF-8?q?=E4=B8=80=E6=99=82=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=8C?= =?UTF-8?q?=E6=97=A2=E5=AD=98=E3=81=AE=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=82=92=E4=B8=8A=E6=9B=B8=E3=81=8D=E3=81=97=E3=81=AA=E3=81=84?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B=20(#2454)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Hiroshiba Kazuyuki --- src/backend/electron/fileHelper.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/backend/electron/fileHelper.ts b/src/backend/electron/fileHelper.ts index 9d80ac0c44..cda62360b6 100644 --- a/src/backend/electron/fileHelper.ts +++ b/src/backend/electron/fileHelper.ts @@ -1,5 +1,7 @@ import fs from "fs"; +import log from "electron-log/main"; import { moveFileSync } from "move-file"; +import { uuid4 } from "@/helpers/random"; /** * 書き込みに失敗したときにファイルが消えないように、 @@ -9,10 +11,19 @@ export function writeFileSafely( path: string, data: string | NodeJS.ArrayBufferView, ) { - const tmpPath = `${path}.tmp`; - fs.writeFileSync(tmpPath, data); + const tmpPath = `${path}-${uuid4()}.tmp`; + fs.writeFileSync(tmpPath, data, { flag: "wx" }); - moveFileSync(tmpPath, path, { - overwrite: true, - }); + try { + moveFileSync(tmpPath, path, { + overwrite: true, + }); + } catch (error) { + if (fs.existsSync(tmpPath)) { + fs.promises.unlink(tmpPath).catch((reason) => { + log.warn("Failed to remove %s\n %o", tmpPath, reason); + }); + } + throw error; + } } From 4149b243ac5547e3e3f9c55c779cf9f4c217aa38 Mon Sep 17 00:00:00 2001 From: "Nanashi." Date: Wed, 1 Jan 2025 21:30:58 +0900 Subject: [PATCH 05/23] =?UTF-8?q?ci:=20=E3=83=9E=E3=83=BC=E3=82=B8?= =?UTF-8?q?=E3=82=AD=E3=83=A5=E3=83=BC=E3=82=92=E8=BF=BD=E5=8A=A0=20(#2456?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/merge_gatekeeper.yml | 29 ++++++++++++++++++++++++++ .github/workflows/test.yml | 2 ++ 2 files changed, 31 insertions(+) create mode 100644 .github/workflows/merge_gatekeeper.yml diff --git a/.github/workflows/merge_gatekeeper.yml b/.github/workflows/merge_gatekeeper.yml new file mode 100644 index 0000000000..92ca76dbd5 --- /dev/null +++ b/.github/workflows/merge_gatekeeper.yml @@ -0,0 +1,29 @@ +name: "Merge Gatekeeper" + +# auto mergeとmerge queue用のチェッカー。 +# Approve数が足りているか、すべてのテストが通っているかを確認します。 +# 詳細: https://github.com/VOICEVOX/merge-gatekeeper + +on: + pull_request_target: + types: [auto_merge_enabled] + merge_group: + types: [checks_requested] + +jobs: + merge_gatekeeper: + runs-on: ubuntu-latest + steps: + - uses: voicevox/merge-gatekeeper@main + with: + token: ${{ secrets.GATEKEEPER_TOKEN }} + required_score: 2 + score_rules: | + #maintainer: 2 + #reviewer: 1 + - uses: upsidr/merge-gatekeeper@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + self: merge_gatekeeper + # https://github.com/upsidr/merge-gatekeeper/issues/71#issuecomment-1660607977 + ref: ${{ github.event.pull_request && github.event.pull_request.head.ref || github.ref }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index de66c30720..b294b3b879 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,6 +5,8 @@ on: pull_request: branches: - "**" + merge_group: + types: [checks_requested] workflow_dispatch: env: From 377850449e5dabc523643cb0a67f2d070957aaa8 Mon Sep 17 00:00:00 2001 From: "Nanashi." Date: Thu, 2 Jan 2025 00:31:09 +0900 Subject: [PATCH 06/23] =?UTF-8?q?fix:=20=E3=83=95=E3=82=A9=E3=83=BC?= =?UTF-8?q?=E3=82=AF=E3=81=8B=E3=82=89=E3=81=A0=E3=81=A8gatekeeper?= =?UTF-8?q?=E3=81=8C=E6=AD=A3=E3=81=97=E3=81=8F=E5=8B=95=E3=81=8B=E3=81=AA?= =?UTF-8?q?=E3=81=84=E3=81=AE=E3=82=92=E6=B2=BB=E3=81=99=20(#2458)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/merge_gatekeeper.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/merge_gatekeeper.yml b/.github/workflows/merge_gatekeeper.yml index 92ca76dbd5..2d3eab72ba 100644 --- a/.github/workflows/merge_gatekeeper.yml +++ b/.github/workflows/merge_gatekeeper.yml @@ -26,4 +26,4 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} self: merge_gatekeeper # https://github.com/upsidr/merge-gatekeeper/issues/71#issuecomment-1660607977 - ref: ${{ github.event.pull_request && github.event.pull_request.head.ref || github.ref }} + ref: ${{ github.event.pull_request && github.event.pull_request.head.sha || github.ref }} From 676e9d36ebb62d420079cf59f41f9be37a80e08b Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Thu, 2 Jan 2025 00:51:19 +0900 Subject: [PATCH 07/23] =?UTF-8?q?refactor:=20=E3=82=B7=E3=83=A7=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=82=AB=E3=83=83=E3=83=88=E3=82=AD=E3=83=BC=E5=91=A8?= =?UTF-8?q?=E3=82=8A=E3=81=AEisMac=E3=82=92{isMac:=20boolean}=E3=81=AB?= =?UTF-8?q?=E3=81=99=E3=82=8B=20(#2447)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/browser/browserConfig.ts | 2 +- src/backend/common/ConfigManager.ts | 38 ++++++++++--------- src/backend/electron/electronConfig.ts | 2 +- src/components/Dialog/HotkeySettingDialog.vue | 4 +- src/domain/hotkeyAction.ts | 6 ++- src/type/preload.ts | 4 +- .../unit/backend/common/configManager.spec.ts | 18 +++++---- tests/unit/domain/hotkeyAction.spec.ts | 2 +- 8 files changed, 43 insertions(+), 33 deletions(-) diff --git a/src/backend/browser/browserConfig.ts b/src/backend/browser/browserConfig.ts index 6508d9af8e..c5ffc900a3 100644 --- a/src/backend/browser/browserConfig.ts +++ b/src/backend/browser/browserConfig.ts @@ -21,7 +21,7 @@ const defaultEngineId = EngineId(defaultEngine.uuid); export async function getConfigManager() { await configManagerLock.acquire("configManager", async () => { if (!configManager) { - configManager = new BrowserConfigManager(isMac); + configManager = new BrowserConfigManager({ isMac }); await configManager.initialize(); } }); diff --git a/src/backend/common/ConfigManager.ts b/src/backend/common/ConfigManager.ts index b5ddabb706..3d64caeb38 100644 --- a/src/backend/common/ConfigManager.ts +++ b/src/backend/common/ConfigManager.ts @@ -16,7 +16,6 @@ import { getDefaultHotkeySettings, HotkeySettingType, } from "@/domain/hotkeyAction"; -import { isMac } from "@/helpers/platform"; const lockKey = "save"; @@ -300,6 +299,7 @@ export type Metadata = { */ export abstract class BaseConfigManager { protected config: ConfigType | undefined; + protected isMac: boolean; private lock = new AsyncLock(); @@ -309,7 +309,9 @@ export abstract class BaseConfigManager { protected abstract getAppVersion(): string; - constructor(protected isMac: boolean) {} + constructor({ isMac }: { isMac: boolean }) { + this.isMac = isMac; + } public reset() { this.config = this.getDefaultConfig(); @@ -326,7 +328,7 @@ export abstract class BaseConfigManager { } } this.config = this.migrateHotkeySettings( - getConfigSchema(this.isMac).parse(data), + getConfigSchema({ isMac: this.isMac }).parse(data), ); this._save(); } else { @@ -357,7 +359,7 @@ export abstract class BaseConfigManager { private _save() { void this.lock.acquire(lockKey, async () => { await this.save({ - ...getConfigSchema(this.isMac).parse({ + ...getConfigSchema({ isMac: this.isMac }).parse({ ...this.config, }), __internal__: { @@ -392,22 +394,22 @@ export abstract class BaseConfigManager { private migrateHotkeySettings(data: ConfigType): ConfigType { const COMBINATION_IS_NONE = HotkeyCombination("####"); const loadedHotkeys = structuredClone(data.hotkeySettings); - const hotkeysWithoutNewCombination = getDefaultHotkeySettings(isMac).map( - (defaultHotkey) => { - const loadedHotkey = loadedHotkeys.find( - (loadedHotkey) => loadedHotkey.action === defaultHotkey.action, - ); - const hotkeyWithoutCombination: HotkeySettingType = { - action: defaultHotkey.action, - combination: COMBINATION_IS_NONE, - }; - return loadedHotkey ?? hotkeyWithoutCombination; - }, - ); + const hotkeysWithoutNewCombination = getDefaultHotkeySettings({ + isMac: this.isMac, + }).map((defaultHotkey) => { + const loadedHotkey = loadedHotkeys.find( + (loadedHotkey) => loadedHotkey.action === defaultHotkey.action, + ); + const hotkeyWithoutCombination: HotkeySettingType = { + action: defaultHotkey.action, + combination: COMBINATION_IS_NONE, + }; + return loadedHotkey ?? hotkeyWithoutCombination; + }); const migratedHotkeys = hotkeysWithoutNewCombination.map((hotkey) => { if (hotkey.combination === COMBINATION_IS_NONE) { const newHotkey = ensureNotNullish( - getDefaultHotkeySettings(isMac).find( + getDefaultHotkeySettings({ isMac: this.isMac }).find( (defaultHotkey) => defaultHotkey.action === hotkey.action, ), ); @@ -434,6 +436,6 @@ export abstract class BaseConfigManager { } protected getDefaultConfig(): ConfigType { - return getConfigSchema(this.isMac).parse({}); + return getConfigSchema({ isMac: this.isMac }).parse({}); } } diff --git a/src/backend/electron/electronConfig.ts b/src/backend/electron/electronConfig.ts index 2c39a083e4..fb61727315 100644 --- a/src/backend/electron/electronConfig.ts +++ b/src/backend/electron/electronConfig.ts @@ -36,7 +36,7 @@ let configManager: ElectronConfigManager | undefined; export function getConfigManager(): ElectronConfigManager { if (!configManager) { - configManager = new ElectronConfigManager(isMac); + configManager = new ElectronConfigManager({ isMac }); } return configManager; } diff --git a/src/components/Dialog/HotkeySettingDialog.vue b/src/components/Dialog/HotkeySettingDialog.vue index 47ebf88726..05fc4c48ec 100644 --- a/src/components/Dialog/HotkeySettingDialog.vue +++ b/src/components/Dialog/HotkeySettingDialog.vue @@ -227,7 +227,7 @@ const setHotkeyDialogOpened = () => { }; const isDefaultCombination = (action: string) => { - const defaultSetting = getDefaultHotkeySettings(isMac).find( + const defaultSetting = getDefaultHotkeySettings({ isMac }).find( (value) => value.action === action, ); const hotkeySetting = hotkeySettings.value.find( @@ -245,7 +245,7 @@ const resetHotkey = async (action: string) => { if (result !== "OK") return; - const setting = getDefaultHotkeySettings(isMac).find( + const setting = getDefaultHotkeySettings({ isMac }).find( (value) => value.action == action, ); if (setting == undefined) { diff --git a/src/domain/hotkeyAction.ts b/src/domain/hotkeyAction.ts index 41ddd8f850..6c92c63556 100644 --- a/src/domain/hotkeyAction.ts +++ b/src/domain/hotkeyAction.ts @@ -60,7 +60,11 @@ export const hotkeySettingSchema = z.object({ }); export type HotkeySettingType = z.infer; -export function getDefaultHotkeySettings(isMac: boolean): HotkeySettingType[] { +export function getDefaultHotkeySettings({ + isMac, +}: { + isMac: boolean; +}): HotkeySettingType[] { return [ { action: "音声書き出し", diff --git a/src/type/preload.ts b/src/type/preload.ts index 24c8f4d6db..f916bc7b29 100644 --- a/src/type/preload.ts +++ b/src/type/preload.ts @@ -402,7 +402,7 @@ export const rootMiscSettingSchema = z.object({ }); export type RootMiscSettingType = z.infer; -export function getConfigSchema(isMac: boolean) { +export function getConfigSchema({ isMac }: { isMac: boolean }) { return z .object({ inheritAudioInfo: z.boolean().default(true), @@ -425,7 +425,7 @@ export function getConfigSchema(isMac: boolean) { .default({}), hotkeySettings: hotkeySettingSchema .array() - .default(getDefaultHotkeySettings(isMac)), + .default(getDefaultHotkeySettings({ isMac })), toolbarSetting: toolbarSettingSchema .array() .default(defaultToolbarButtonSetting), diff --git a/tests/unit/backend/common/configManager.spec.ts b/tests/unit/backend/common/configManager.spec.ts index eed9aa0aaa..09945dd08a 100644 --- a/tests/unit/backend/common/configManager.spec.ts +++ b/tests/unit/backend/common/configManager.spec.ts @@ -4,7 +4,7 @@ import { BaseConfigManager } from "@/backend/common/ConfigManager"; import { getConfigSchema } from "@/type/preload"; const configBase = { - ...getConfigSchema(false).parse({}), + ...getConfigSchema({ isMac: false }).parse({}), __internal__: { migrations: { version: "999.999.999", @@ -13,6 +13,10 @@ const configBase = { }; class TestConfigManager extends BaseConfigManager { + constructor() { + super({ isMac: false }); + } + getAppVersion() { return "999.999.999"; } @@ -49,7 +53,7 @@ it("新規作成できる", async () => { async () => undefined, ); - const configManager = new TestConfigManager(false); + const configManager = new TestConfigManager(); await configManager.initialize(); expect(configManager).toBeTruthy(); }); @@ -62,7 +66,7 @@ it("バージョンが保存される", async () => { .spyOn(TestConfigManager.prototype, "save") .mockImplementation(async () => undefined); - const configManager = new TestConfigManager(false); + const configManager = new TestConfigManager(); await configManager.initialize(); await configManager.ensureSaved(); expect(saveSpy).toHaveBeenCalled(); @@ -82,7 +86,7 @@ for (const [version, data] of pastConfigs) { async () => data, ); - const configManager = new TestConfigManager(false); + const configManager = new TestConfigManager(); await configManager.initialize(); expect(configManager).toBeTruthy(); @@ -122,7 +126,7 @@ it("0.19.1からのマイグレーション時にハミング・ソングスタ ).map((key) => getStyleIdFromVoiceId(key)); // マイグレーション - const configManager = new TestConfigManager(false); + const configManager = new TestConfigManager(); await configManager.initialize(); const presets = configManager.get("presets"); const defaultPresetKeys = configManager.get("defaultPresetKeys"); @@ -164,7 +168,7 @@ it("getできる", async () => { }), ); - const configManager = new TestConfigManager(false); + const configManager = new TestConfigManager(); await configManager.initialize(); expect(configManager.get("inheritAudioInfo")).toBe(false); }); @@ -183,7 +187,7 @@ it("setできる", async () => { }), ); - const configManager = new TestConfigManager(false); + const configManager = new TestConfigManager(); await configManager.initialize(); configManager.set("inheritAudioInfo", true); expect(configManager.get("inheritAudioInfo")).toBe(true); diff --git a/tests/unit/domain/hotkeyAction.spec.ts b/tests/unit/domain/hotkeyAction.spec.ts index 9d1f90e397..5124fa140e 100644 --- a/tests/unit/domain/hotkeyAction.spec.ts +++ b/tests/unit/domain/hotkeyAction.spec.ts @@ -4,7 +4,7 @@ import { } from "@/domain/hotkeyAction"; test("すべてのホットキーに初期値が設定されている", async () => { - const defaultHotkeySettings = getDefaultHotkeySettings(false); + const defaultHotkeySettings = getDefaultHotkeySettings({ isMac: false }); const allActionNames = new Set(hotkeyActionNameSchema.options); const defaultHotkeyActionsNames = new Set( defaultHotkeySettings.map((setting) => setting.action), From 18998167de481ddcff3891c671781a280b28a8f1 Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Thu, 2 Jan 2025 01:03:15 +0900 Subject: [PATCH 08/23] =?UTF-8?q?feat:=20VVPP=E3=83=87=E3=83=95=E3=82=A9?= =?UTF-8?q?=E3=83=AB=E3=83=88=E3=82=A8=E3=83=B3=E3=82=B8=E3=83=B3=E3=81=AE?= =?UTF-8?q?=E9=80=B2=E6=8D=97=E3=82=92=E5=87=BA=E3=81=9B=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E3=81=97=E3=80=81=E3=82=AA=E3=83=B3=E3=83=A1?= =?UTF-8?q?=E3=83=A2=E3=83=AA=E3=81=98=E3=82=83=E3=81=AA=E3=81=8F=E3=81=99?= =?UTF-8?q?=E3=82=8B=20(#2453)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nanashi. --- .../electron/engineAndVvppController.ts | 51 +++++++++++++++---- src/backend/electron/main.ts | 11 ++++ src/backend/electron/manager/vvppManager.ts | 49 ++++++++++++++---- src/backend/electron/type.ts | 6 +++ 4 files changed, 96 insertions(+), 21 deletions(-) create mode 100644 src/backend/electron/type.ts diff --git a/src/backend/electron/engineAndVvppController.ts b/src/backend/electron/engineAndVvppController.ts index 076a484570..c30c4e1311 100644 --- a/src/backend/electron/engineAndVvppController.ts +++ b/src/backend/electron/engineAndVvppController.ts @@ -1,5 +1,6 @@ import path from "path"; import fs from "fs"; +import { ReadableStream } from "node:stream/web"; import log from "electron-log/main"; import { BrowserWindow, dialog } from "electron"; @@ -8,6 +9,7 @@ import { getEngineInfoManager } from "./manager/engineInfoManager"; import { getEngineProcessManager } from "./manager/engineProcessManager"; import { getRuntimeInfoManager } from "./manager/RuntimeInfoManager"; import { getVvppManager } from "./manager/vvppManager"; +import { ProgressCallback } from "./type"; import { EngineId, EngineInfo, @@ -45,9 +47,12 @@ export class EngineAndVvppController { /** * VVPPエンジンをインストールする。 */ - async installVvppEngine(vvppPath: string) { + async installVvppEngine( + vvppPath: string, + callbacks?: { onProgress?: ProgressCallback }, + ) { try { - await this.vvppManager.install(vvppPath); + await this.vvppManager.install(vvppPath, callbacks); return true; } catch (e) { log.error(`Failed to install ${vvppPath},`, e); @@ -184,6 +189,7 @@ export class EngineAndVvppController { async downloadAndInstallVvppEngine( downloadDir: string, packageInfo: PackageInfo, + callbacks: { onProgress: ProgressCallback<"download" | "install"> }, ) { if (packageInfo.packages.length === 0) { throw new UnreachableError("No packages to download"); @@ -193,27 +199,52 @@ export class EngineAndVvppController { const downloadedPaths: string[] = []; try { // ダウンロード + callbacks.onProgress({ type: "download", progress: 0 }); + + let totalBytes = 0; + packageInfo.packages.forEach((p) => { + totalBytes += p.size; + }); + + let downloadedBytes = 0; await Promise.all( packageInfo.packages.map(async (p) => { - const { url, name, size } = p; - - log.info(`Download ${name} from ${url}, size: ${size}`); - const res = await fetch(url); - const buffer = await res.arrayBuffer(); if (failed) return; // 他のダウンロードが失敗していたら中断 + const { url, name } = p; + log.info(`Download ${name} from ${url}`); + + const res = await fetch(url); + if (!res.ok || res.body == null) + throw new Error(`Failed to download ${name} from ${url}`); const downloadPath = path.join(downloadDir, name); - await fs.promises.writeFile(downloadPath, Buffer.from(buffer)); // TODO: オンメモリじゃなくする - log.info(`Downloaded ${name} to ${downloadPath}`); + const fileStream = fs.createWriteStream(downloadPath); + + // ファイルに書き込む + // NOTE: なぜか型が合わないのでasを使っている + for await (const chunk of res.body as ReadableStream) { + fileStream.write(chunk); + downloadedBytes += chunk.length; + callbacks.onProgress({ + type: "download", + progress: (downloadedBytes / totalBytes) * 100, + }); + } + fileStream.close(); downloadedPaths.push(downloadPath); + log.info(`Downloaded ${name} to ${downloadPath}`); // TODO: ハッシュチェック }), ); // インストール - await this.installVvppEngine(downloadedPaths[0]); + await this.installVvppEngine(downloadedPaths[0], { + onProgress: ({ progress }) => { + callbacks.onProgress({ type: "install", progress }); + }, + }); } catch (e) { failed = true; log.error(`Failed to download and install VVPP engine:`, e); diff --git a/src/backend/electron/main.ts b/src/backend/electron/main.ts index be092f8a26..abac4649e5 100644 --- a/src/backend/electron/main.ts +++ b/src/backend/electron/main.ts @@ -948,9 +948,20 @@ app.on("ready", async () => { } // ダウンロードしてインストールする + let lastLogTime = 0; // とりあえずログを0.1秒に1回だけ出力する await engineAndVvppController.downloadAndInstallVvppEngine( app.getPath("downloads"), packageInfo, + { + onProgress: ({ type, progress }) => { + if (Date.now() - lastLogTime > 100) { + log.info( + `VVPP default engine progress: ${type}: ${Math.floor(progress)}%`, + ); + lastLogTime = Date.now(); + } + }, + }, ); } diff --git a/src/backend/electron/manager/vvppManager.ts b/src/backend/electron/manager/vvppManager.ts index edd3177df7..9484a8476e 100644 --- a/src/backend/electron/manager/vvppManager.ts +++ b/src/backend/electron/manager/vvppManager.ts @@ -7,6 +7,7 @@ import { app, dialog } from "electron"; import MultiStream from "multistream"; import { glob } from "glob"; import AsyncLock from "async-lock"; +import { ProgressCallback } from "../type"; import { EngineId, EngineInfo, @@ -109,7 +110,10 @@ export class VvppManager { private async extractVvpp( vvppLikeFilePath: string, + callbacks?: { onProgress?: ProgressCallback }, ): Promise<{ outputDir: string; manifest: MinimumEngineManifestType }> { + callbacks?.onProgress?.({ progress: 0 }); + const nonce = new Date().getTime().toString(); const outputDir = path.join(this.vvppEngineDir, ".tmp", nonce); @@ -182,7 +186,13 @@ export class VvppManager { log.log("Single file, not concatenating"); } - const args = ["x", "-o" + outputDir, archiveFile, "-t" + format]; + const args = [ + "x", + "-o" + outputDir, + archiveFile, + "-t" + format, + "-bsp1", // 進捗出力 + ]; let sevenZipPath = import.meta.env.VITE_7Z_BIN_NAME; if (!sevenZipPath) { @@ -194,18 +204,27 @@ export class VvppManager { sevenZipPath, ); } - log.log( - "Spawning 7z:", - sevenZipPath, - args.map((a) => JSON.stringify(a)).join(" "), - ); + log.log("Spawning 7z:", sevenZipPath, args.join(" ")); await new Promise((resolve, reject) => { const child = spawn(sevenZipPath, args, { stdio: ["pipe", "pipe", "pipe"], }); child.stdout?.on("data", (data: Buffer) => { - log.info(`7z STDOUT: ${data.toString("utf-8")}`); + const output = data.toString("utf-8"); + log.info(`7z STDOUT: ${output}`); + + // 進捗を取得 + // NOTE: ` 75% 106 - pyopenjtalk\open_jtalk_dic_utf_8-1.11\sys.dic` のような出力が来る + // TODO: 出力が変わるかもしれないのでテストが必要 + const progressMatch = output.match( + / *(?\d+)% ?(?\d+)? ?(?.*)/, + ); + if (progressMatch?.groups?.percent) { + callbacks?.onProgress?.({ + progress: parseInt(progressMatch.groups.percent), + }); + } }); child.stderr?.on("data", (data: Buffer) => { @@ -214,6 +233,7 @@ export class VvppManager { child.on("exit", (code) => { if (code === 0) { + callbacks?.onProgress?.({ progress: 100 }); resolve(); } else { reject(new Error(`7z exited with code ${code}`)); @@ -253,11 +273,18 @@ export class VvppManager { /** * 追加 */ - async install(vvppPath: string) { - await this.lock.acquire(lockKey, () => this._install(vvppPath)); + async install( + vvppPath: string, + callbacks?: { onProgress?: ProgressCallback }, + ) { + await this.lock.acquire(lockKey, () => this._install(vvppPath, callbacks)); } - private async _install(vvppPath: string) { - const { outputDir, manifest } = await this.extractVvpp(vvppPath); + private async _install( + vvppPath: string, + callbacks?: { onProgress?: ProgressCallback }, + ) { + const { outputDir, manifest } = await this.extractVvpp(vvppPath, callbacks); + const dirName = this.toValidDirName(manifest); const engineDirectory = path.join(this.vvppEngineDir, dirName); const oldEngineDirName = ( diff --git a/src/backend/electron/type.ts b/src/backend/electron/type.ts new file mode 100644 index 0000000000..63c83eedca --- /dev/null +++ b/src/backend/electron/type.ts @@ -0,0 +1,6 @@ +/** 進捗を返すコールバック */ +export type ProgressCallback = [T] extends [ + void, +] + ? (payload: { progress: number }) => void + : (payload: { type: T; progress: number }) => void; From e5d33ba9ab2a3e96f908c4732a3114aade13eb0e Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Thu, 2 Jan 2025 10:45:37 +0900 Subject: [PATCH 09/23] =?UTF-8?q?ci:=20merge-gatekeeyper=E3=81=AEtimeout?= =?UTF-8?q?=E3=82=92=E4=BC=B8=E3=81=B0=E3=81=99=20(#2460)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/merge_gatekeeper.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/merge_gatekeeper.yml b/.github/workflows/merge_gatekeeper.yml index 2d3eab72ba..d497e0d210 100644 --- a/.github/workflows/merge_gatekeeper.yml +++ b/.github/workflows/merge_gatekeeper.yml @@ -27,3 +27,4 @@ jobs: self: merge_gatekeeper # https://github.com/upsidr/merge-gatekeeper/issues/71#issuecomment-1660607977 ref: ${{ github.event.pull_request && github.event.pull_request.head.sha || github.ref }} + timeout: 18000 # 5 hours From fea33347249c39b177cc4283b46fdacc0920b071 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Thu, 2 Jan 2025 19:18:24 +0900 Subject: [PATCH 10/23] =?UTF-8?q?refactor:=20WindowManager=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=20(#2455)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Hiroshiba Co-authored-by: Hiroshiba Kazuyuki --- .../electron/engineAndVvppController.ts | 12 +- src/backend/electron/main.ts | 300 +++++------------ src/backend/electron/manager/windowManager.ts | 315 ++++++++++++++++++ tests/e2e/electron/example.spec.ts | 3 +- 4 files changed, 398 insertions(+), 232 deletions(-) create mode 100644 src/backend/electron/manager/windowManager.ts diff --git a/src/backend/electron/engineAndVvppController.ts b/src/backend/electron/engineAndVvppController.ts index c30c4e1311..cc4bd95952 100644 --- a/src/backend/electron/engineAndVvppController.ts +++ b/src/backend/electron/engineAndVvppController.ts @@ -2,13 +2,14 @@ import path from "path"; import fs from "fs"; import { ReadableStream } from "node:stream/web"; import log from "electron-log/main"; -import { BrowserWindow, dialog } from "electron"; +import { dialog } from "electron"; import { getConfigManager } from "./electronConfig"; import { getEngineInfoManager } from "./manager/engineInfoManager"; import { getEngineProcessManager } from "./manager/engineProcessManager"; import { getRuntimeInfoManager } from "./manager/RuntimeInfoManager"; import { getVvppManager } from "./manager/vvppManager"; +import { getWindowManager } from "./manager/windowManager"; import { ProgressCallback } from "./type"; import { EngineId, @@ -72,14 +73,13 @@ export class EngineAndVvppController { vvppPath, reloadNeeded, reloadCallback, - win, }: { vvppPath: string; reloadNeeded: boolean; reloadCallback?: () => void; // 再読み込みが必要な場合のコールバック - win: BrowserWindow; // dialog表示に必要。 FIXME: dialog表示関数をDI可能にし、winを削除する }) { - const result = dialog.showMessageBoxSync(win, { + const windowManager = getWindowManager(); + const result = windowManager.showMessageBoxSync({ type: "warning", title: "エンジン追加の確認", message: `この操作はコンピュータに損害を与える可能性があります。エンジンの配布元が信頼できない場合は追加しないでください。`, @@ -94,8 +94,8 @@ export class EngineAndVvppController { await this.installVvppEngine(vvppPath); if (reloadNeeded) { - void dialog - .showMessageBox(win, { + void windowManager + .showMessageBox({ type: "info", title: "再読み込みが必要です", message: diff --git a/src/backend/electron/main.ts b/src/backend/electron/main.ts index abac4649e5..0bee5375cc 100644 --- a/src/backend/electron/main.ts +++ b/src/backend/electron/main.ts @@ -4,21 +4,11 @@ import path from "path"; import fs from "fs"; import { pathToFileURL } from "url"; -import { - app, - protocol, - BrowserWindow, - dialog, - Menu, - shell, - nativeTheme, - net, -} from "electron"; +import { app, dialog, Menu, nativeTheme, net, protocol, shell } from "electron"; import installExtension, { VUEJS_DEVTOOLS } from "electron-devtools-installer"; import log from "electron-log/main"; import dayjs from "dayjs"; -import windowStateKeeper from "electron-window-state"; import { hasSupportedGpu } from "./device"; import { getEngineInfoManager, @@ -29,6 +19,10 @@ import { initializeEngineProcessManager, } from "./manager/engineProcessManager"; import { initializeVvppManager, isVvppFile } from "./manager/vvppManager"; +import { + getWindowManager, + initializeWindowManager, +} from "./manager/windowManager"; import configMigration014 from "./configMigration014"; import { initializeRuntimeInfoManager } from "./manager/RuntimeInfoManager"; import { registerIpcMainHandle, ipcMainSendProxy, IpcMainHandle } from "./ipc"; @@ -44,7 +38,6 @@ import { EngineId, TextAsset, } from "@/type/preload"; -import { themes } from "@/domain/theme"; import { isMac } from "@/helpers/platform"; type SingleInstanceLockData = { @@ -110,8 +103,6 @@ if (errorForRemoveBeforeUserDataDir != undefined) { log.error(errorForRemoveBeforeUserDataDir); } -let win: BrowserWindow; - process.on("uncaughtException", (error) => { log.error(error); @@ -151,7 +142,28 @@ protocol.registerSchemesAsPrivileged([ { scheme: "app", privileges: { secure: true, standard: true, stream: true } }, ]); -const firstUrl = import.meta.env.VITE_DEV_SERVER_URL ?? "app://./index.html"; +// ソフトウェア起動時はプロトコルを app にする +void app.whenReady().then(() => { + protocol.handle("app", (request) => { + // 読み取り先のファイルがインストールディレクトリ内であることを確認する + // 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()); + }); +}); // engine const vvppEngineDir = path.join(app.getPath("userData"), "vvpp-engines"); @@ -166,6 +178,7 @@ const onEngineProcessError = (engineInfo: EngineInfo, error: Error) => { // winが作られる前にエラーが発生した場合はwinへの通知を諦める // FIXME: winが作られた後にエンジンを起動させる + const win = windowManager.win; if (win != undefined) { ipcMainSendProxy.DETECTED_ENGINE_ERROR(win, { engineId }); } else { @@ -175,6 +188,16 @@ const onEngineProcessError = (engineInfo: EngineInfo, error: Error) => { dialog.showErrorBox("音声合成エンジンエラー", error.message); }; +const appState = { + willQuit: false, +}; + +initializeWindowManager({ + appStateGetter: () => appState, + isDevelopment, + isTest, + staticDir: __static, +}); initializeRuntimeInfoManager({ runtimeInfoPath: path.join(app.getPath("userData"), "runtime-info.json"), appVersion: app.getVersion(), @@ -187,6 +210,7 @@ initializeEngineProcessManager({ onEngineProcessError }); initializeVvppManager({ vvppEngineDir }); const configManager = getConfigManager(); +const windowManager = getWindowManager(); const engineInfoManager = getEngineInfoManager(); const engineProcessManager = getEngineProcessManager(); const engineAndVvppController = getEngineAndVvppController(); @@ -207,7 +231,7 @@ function openEngineDirectory(engineId: EngineId) { function checkMultiEngineEnabled(): boolean { const enabled = configManager.get("enableMultiEngine"); if (!enabled) { - dialog.showMessageBoxSync(win, { + windowManager.showMessageBoxSync({ type: "info", title: "マルチエンジン機能が無効です", message: `マルチエンジン機能が無効です。vvppファイルを使用するには設定からマルチエンジン機能を有効にしてください。`, @@ -218,148 +242,7 @@ function checkMultiEngineEnabled(): boolean { return enabled; } -const appState = { - willQuit: false, -}; let filePathOnMac: string | undefined = undefined; -// create window -async function createWindow() { - const mainWindowState = windowStateKeeper({ - defaultWidth: 1024, - defaultHeight: 630, - }); - - const currentTheme = configManager.get("currentTheme"); - const backgroundColor = themes.find((value) => value.name == currentTheme) - ?.colors.background; - - win = new BrowserWindow({ - x: mainWindowState.x, - y: mainWindowState.y, - width: mainWindowState.width, - height: mainWindowState.height, - frame: false, - titleBarStyle: "hidden", - trafficLightPosition: { x: 6, y: 4 }, - minWidth: 320, - show: false, - backgroundColor, - webPreferences: { - preload: path.join(__dirname, "preload.js"), - }, - icon: path.join(__static, "icon.png"), - }); - - let projectFilePath = ""; - if (isMac) { - if (filePathOnMac) { - if (filePathOnMac.endsWith(".vvproj")) { - projectFilePath = filePathOnMac; - } - filePathOnMac = undefined; - } - } else { - if (process.argv.length >= 2) { - const filePath = process.argv[1]; - if ( - fs.existsSync(filePath) && - fs.statSync(filePath).isFile() && - filePath.endsWith(".vvproj") - ) { - projectFilePath = filePath; - } - } - } - - // ソフトウェア起動時はプロトコルを app にする - if (import.meta.env.VITE_DEV_SERVER_URL == undefined) { - protocol.handle("app", (request) => { - // 読み取り先のファイルがインストールディレクトリ内であることを確認する - // 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()); - }); - } - - await loadUrl({ projectFilePath }); - - if (isDevelopment && !isTest) win.webContents.openDevTools(); - - win.on("maximize", () => { - ipcMainSendProxy.DETECT_MAXIMIZED(win); - }); - win.on("unmaximize", () => { - ipcMainSendProxy.DETECT_UNMAXIMIZED(win); - }); - win.on("enter-full-screen", () => { - ipcMainSendProxy.DETECT_ENTER_FULLSCREEN(win); - }); - win.on("leave-full-screen", () => { - ipcMainSendProxy.DETECT_LEAVE_FULLSCREEN(win); - }); - win.on("always-on-top-changed", () => { - win.isAlwaysOnTop() - ? ipcMainSendProxy.DETECT_PINNED(win) - : ipcMainSendProxy.DETECT_UNPINNED(win); - }); - win.on("close", (event) => { - if (!appState.willQuit) { - event.preventDefault(); - ipcMainSendProxy.CHECK_EDITED_AND_NOT_SAVE(win, { - closeOrReload: "close", - }); - return; - } - }); - - win.on("resize", () => { - const windowSize = win.getSize(); - ipcMainSendProxy.DETECT_RESIZED(win, { - width: windowSize[0], - height: windowSize[1], - }); - }); - - mainWindowState.manage(win); -} - -/** - * 画面の読み込みを開始する。 - * @param obj.isMultiEngineOffMode マルチエンジンオフモードにするかどうか。無指定時はfalse扱いになる。 - * @param obj.projectFilePath 初期化時に読み込むプロジェクトファイル。無指定時は何も読み込まない。 - * @returns ロードの完了を待つPromise。 - */ -async function loadUrl(obj: { - isMultiEngineOffMode?: boolean; - projectFilePath?: string; -}) { - const url = new URL(firstUrl); - url.searchParams.append( - "isMultiEngineOffMode", - (obj?.isMultiEngineOffMode ?? false).toString(), - ); - url.searchParams.append("projectFilePath", obj?.projectFilePath ?? ""); - return win.loadURL(url.toString()); -} - -// 開始。その他の準備が完了した後に呼ばれる。 -async function start() { - await engineAndVvppController.launchEngines(); - await createWindow(); -} const menuTemplateForMac: Electron.MenuItemConstructorOptions[] = [ { @@ -416,7 +299,7 @@ const retryShowSaveDialogWhileSafeDir = async < */ const showWarningDialog = async () => { const productName = app.getName().toUpperCase(); - const warningResult = await dialog.showMessageBox(win, { + const warningResult = await windowManager.showMessageBox({ message: `指定された保存先は${productName}により自動的に削除される可能性があります。\n他の場所に保存することをおすすめします。`, type: "warning", buttons: ["保存場所を変更", "無視して保存"], @@ -474,7 +357,7 @@ registerIpcMainHandle({ */ SHOW_SAVE_DIRECTORY_DIALOG: async (_, { title }) => { const result = await retryShowSaveDialogWhileSafeDir(() => - dialog.showOpenDialog(win, { + windowManager.showOpenDialog({ title, properties: [ "openDirectory", @@ -490,7 +373,7 @@ registerIpcMainHandle({ }, SHOW_VVPP_OPEN_DIALOG: async (_, { title, defaultPath }) => { - const result = await dialog.showOpenDialog(win, { + const result = await windowManager.showOpenDialog({ title, defaultPath, filters: [ @@ -506,7 +389,7 @@ registerIpcMainHandle({ * 保存先として選ぶ場合は SHOW_SAVE_DIRECTORY_DIALOG を使うべき。 */ SHOW_OPEN_DIRECTORY_DIALOG: async (_, { title }) => { - const result = await dialog.showOpenDialog(win, { + const result = await windowManager.showOpenDialog({ title, properties: [ "openDirectory", @@ -522,7 +405,7 @@ registerIpcMainHandle({ SHOW_PROJECT_SAVE_DIALOG: async (_, { title, defaultPath }) => { const result = await retryShowSaveDialogWhileSafeDir(() => - dialog.showSaveDialog(win, { + windowManager.showSaveDialog({ title, defaultPath, filters: [{ name: "VOICEVOX Project file", extensions: ["vvproj"] }], @@ -536,7 +419,7 @@ registerIpcMainHandle({ }, SHOW_PROJECT_LOAD_DIALOG: async (_, { title }) => { - const result = await dialog.showOpenDialog(win, { + const result = await windowManager.showOpenDialog({ title, filters: [{ name: "VOICEVOX Project file", extensions: ["vvproj"] }], properties: ["openFile", "createDirectory", "treatPackageAsDirectory"], @@ -548,7 +431,7 @@ registerIpcMainHandle({ }, SHOW_WARNING_DIALOG: (_, { title, message }) => { - return dialog.showMessageBox(win, { + return windowManager.showMessageBox({ type: "warning", title, message, @@ -556,7 +439,7 @@ registerIpcMainHandle({ }, SHOW_ERROR_DIALOG: (_, { title, message }) => { - return dialog.showMessageBox(win, { + return windowManager.showMessageBox({ type: "error", title, message, @@ -564,7 +447,7 @@ registerIpcMainHandle({ }, SHOW_IMPORT_FILE_DIALOG: (_, { title, name, extensions }) => { - return dialog.showOpenDialogSync(win, { + return windowManager.showOpenDialogSync({ title, filters: [{ name: name ?? "Text", extensions: extensions ?? ["txt"] }], properties: ["openFile", "createDirectory", "treatPackageAsDirectory"], @@ -576,7 +459,7 @@ registerIpcMainHandle({ { title, defaultPath, extensionName, extensions }, ) => { const result = await retryShowSaveDialogWhileSafeDir(() => - dialog.showSaveDialog(win, { + windowManager.showSaveDialog({ title, defaultPath, filters: [{ name: extensionName, extensions: extensions }], @@ -591,48 +474,33 @@ registerIpcMainHandle({ }, IS_MAXIMIZED_WINDOW: () => { - return win.isMaximized(); + return windowManager.isMaximized(); }, CLOSE_WINDOW: () => { appState.willQuit = true; - win.destroy(); + windowManager.destroyWindow(); }, MINIMIZE_WINDOW: () => { - win.minimize(); + windowManager.minimize(); }, TOGGLE_MAXIMIZE_WINDOW: () => { - // 全画面表示中は、全画面表示解除のみを行い、最大化解除処理は実施しない - if (win.isFullScreen()) { - win.setFullScreen(false); - } else if (win.isMaximized()) { - win.unmaximize(); - } else { - win.maximize(); - } + windowManager.toggleMaximizeWindow(); }, TOGGLE_FULLSCREEN: () => { - if (win.isFullScreen()) { - win.setFullScreen(false); - } else { - win.setFullScreen(true); - } + windowManager.toggleFullScreen(); }, /** UIの拡大 */ ZOOM_IN: () => { - win.webContents.setZoomFactor( - Math.min(Math.max(win.webContents.getZoomFactor() + 0.1, 0.5), 3), - ); + windowManager.zoomIn(); }, /** UIの縮小 */ ZOOM_OUT: () => { - win.webContents.setZoomFactor( - Math.min(Math.max(win.webContents.getZoomFactor() - 0.1, 0.5), 3), - ); + windowManager.zoomOut(); }, /** UIの拡大率リセット */ ZOOM_RESET: () => { - win.webContents.setZoomFactor(1); + windowManager.zoomReset(); }, OPEN_LOG_DIRECTORY: () => { void shell.openPath(app.getPath("logs")); @@ -666,18 +534,14 @@ registerIpcMainHandle({ }, ON_VUEX_READY: () => { - win.show(); + windowManager.show(); }, CHECK_FILE_EXISTS: (_, { file }) => { return fs.existsSync(file); }, CHANGE_PIN_WINDOW: () => { - if (win.isAlwaysOnTop()) { - win.setAlwaysOnTop(false); - } else { - win.setAlwaysOnTop(true); - } + windowManager.togglePinWindow(); }, GET_DEFAULT_TOOLBAR_SETTING: () => { @@ -714,24 +578,7 @@ registerIpcMainHandle({ }, RELOAD_APP: async (_, { isMultiEngineOffMode }) => { - win.hide(); // FIXME: ダミーページ表示のほうが良い - - // 一旦適当なURLに飛ばしてページをアンロードする - await win.loadURL("about:blank"); - - log.info("Checking ENGINE status before reload app"); - const engineCleanupResult = engineAndVvppController.cleanupEngines(); - - // エンジンの停止とエンジン終了後処理の待機 - if (engineCleanupResult != "alreadyCompleted") { - await engineCleanupResult; - } - log.info("Post engine kill process done. Now reloading app"); - - await engineAndVvppController.launchEngines(); - - await loadUrl({ isMultiEngineOffMode: !!isMultiEngineOffMode }); - win.show(); + await windowManager.reload(isMultiEngineOffMode); }, WRITE_FILE: (_, { filePath, buffer }) => { @@ -792,7 +639,9 @@ app.on("window-all-closed", () => { app.on("before-quit", async (event) => { if (!appState.willQuit) { event.preventDefault(); - ipcMainSendProxy.CHECK_EDITED_AND_NOT_SAVE(win, { closeOrReload: "close" }); + ipcMainSendProxy.CHECK_EDITED_AND_NOT_SAVE(windowManager.getWindow(), { + closeOrReload: "close", + }); return; } @@ -841,7 +690,7 @@ app.once("will-finish-launching", () => { }); }); -app.on("ready", async () => { +void app.whenReady().then(async () => { await configManager.initialize().catch(async (e) => { log.error(e); @@ -936,7 +785,7 @@ app.on("ready", async () => { await engineAndVvppController.fetchInsallablePackageInfos(); for (const { engineName, packageInfo } of packageInfos) { // インストールするか確認 - const result = dialog.showMessageBoxSync(win, { + const result = dialog.showMessageBoxSync({ type: "info", title: "デフォルトエンジンのインストール", message: `${engineName} をインストールしますか?`, @@ -976,6 +825,7 @@ app.on("ready", async () => { } // 多重起動防止 + // TODO: readyを待たずにもっと早く実行すべき if ( !isDevelopment && !isTest && @@ -997,17 +847,23 @@ app.on("ready", async () => { await engineAndVvppController.installVvppEngineWithWarning({ vvppPath: filePath, reloadNeeded: false, - win, }); } } - void start(); + await engineAndVvppController.launchEngines(); + await windowManager.createWindow(filePath); }); // 他のプロセスが起動したとき、`requestSingleInstanceLock`経由で`rawData`が送信される。 app.on("second-instance", async (_event, _argv, _workDir, rawData) => { const data = rawData as SingleInstanceLockData; + const win = windowManager.win; + if (win == undefined) { + // TODO: 起動シーケンス中の場合はWindowが作られるまで待つ + log.warn("A 'second-instance' event was emitted but there is no window."); + return; + } if (!data.filePath) { log.info("No file path sent"); } else if (isVvppFile(data.filePath)) { @@ -1022,7 +878,6 @@ app.on("second-instance", async (_event, _argv, _workDir, rawData) => { closeOrReload: "reload", }); }, - win, }); } } else if (data.filePath.endsWith(".vvproj")) { @@ -1031,10 +886,7 @@ app.on("second-instance", async (_event, _argv, _workDir, rawData) => { filePath: data.filePath, }); } - if (win) { - if (win.isMinimized()) win.restore(); - win.focus(); - } + windowManager.restoreAndFocus(); }); if (isDevelopment) { diff --git a/src/backend/electron/manager/windowManager.ts b/src/backend/electron/manager/windowManager.ts new file mode 100644 index 0000000000..c0a49734af --- /dev/null +++ b/src/backend/electron/manager/windowManager.ts @@ -0,0 +1,315 @@ +import fs from "fs"; +import path from "path"; +import { + BrowserWindow, + dialog, + MessageBoxOptions, + MessageBoxSyncOptions, + OpenDialogOptions, + OpenDialogSyncOptions, + SaveDialogOptions, +} from "electron"; +import log from "electron-log/main"; +import windowStateKeeper from "electron-window-state"; +import { getConfigManager } from "../electronConfig"; +import { getEngineAndVvppController } from "../engineAndVvppController"; +import { ipcMainSendProxy } from "../ipc"; +import { isMac } from "@/helpers/platform"; +import { themes } from "@/domain/theme"; + +type WindowManagerOption = { + appStateGetter: () => { willQuit: boolean }; + staticDir: string; + isDevelopment: boolean; + isTest: boolean; +}; + +class WindowManager { + private _win: BrowserWindow | undefined; + private appStateGetter: () => { willQuit: boolean }; + private staticDir: string; + private isDevelopment: boolean; + private isTest: boolean; + + constructor(payload: WindowManagerOption) { + this.appStateGetter = payload.appStateGetter; + this.staticDir = payload.staticDir; + this.isDevelopment = payload.isDevelopment; + this.isTest = payload.isTest; + } + + /** + * BrowserWindowを取得する + */ + public get win() { + return this._win; + } + + /** + * BrowserWindowを取得するが存在しない場合は例外を投げる + */ + public getWindow() { + if (this._win == undefined) { + throw new Error("_win == undefined"); + } + return this._win; + } + + public async createWindow(filePathOnMac: string | undefined) { + if (this.win != undefined) { + throw new Error("Window has already been created"); + } + const mainWindowState = windowStateKeeper({ + defaultWidth: 1024, + defaultHeight: 630, + }); + + const configManager = getConfigManager(); + const currentTheme = configManager.get("currentTheme"); + const backgroundColor = themes.find((value) => value.name == currentTheme) + ?.colors.background; + + const win = new BrowserWindow({ + x: mainWindowState.x, + y: mainWindowState.y, + width: mainWindowState.width, + height: mainWindowState.height, + frame: false, + titleBarStyle: "hidden", + trafficLightPosition: { x: 6, y: 4 }, + minWidth: 320, + show: false, + backgroundColor, + webPreferences: { + preload: path.join(__dirname, "preload.js"), + }, + icon: path.join(this.staticDir, "icon.png"), + }); + + let projectFilePath = ""; + if (isMac) { + if (filePathOnMac) { + if (filePathOnMac.endsWith(".vvproj")) { + projectFilePath = filePathOnMac; + } + filePathOnMac = undefined; + } + } else { + if (process.argv.length >= 2) { + const filePath = process.argv[1]; + if ( + fs.existsSync(filePath) && + fs.statSync(filePath).isFile() && + filePath.endsWith(".vvproj") + ) { + projectFilePath = filePath; + } + } + } + + win.on("maximize", () => { + ipcMainSendProxy.DETECT_MAXIMIZED(win); + }); + win.on("unmaximize", () => { + ipcMainSendProxy.DETECT_UNMAXIMIZED(win); + }); + win.on("enter-full-screen", () => { + ipcMainSendProxy.DETECT_ENTER_FULLSCREEN(win); + }); + win.on("leave-full-screen", () => { + ipcMainSendProxy.DETECT_LEAVE_FULLSCREEN(win); + }); + win.on("always-on-top-changed", () => { + win.isAlwaysOnTop() + ? ipcMainSendProxy.DETECT_PINNED(win) + : ipcMainSendProxy.DETECT_UNPINNED(win); + }); + win.on("close", (event) => { + const appState = this.appStateGetter(); + if (!appState.willQuit) { + event.preventDefault(); + ipcMainSendProxy.CHECK_EDITED_AND_NOT_SAVE(win, { + closeOrReload: "close", + }); + return; + } + }); + win.on("closed", () => { + this._win = undefined; + }); + win.on("resize", () => { + const windowSize = win.getSize(); + ipcMainSendProxy.DETECT_RESIZED(win, { + width: windowSize[0], + height: windowSize[1], + }); + }); + mainWindowState.manage(win); + this._win = win; + + await this.load({ projectFilePath }); + + if (this.isDevelopment && !this.isTest) win.webContents.openDevTools(); + } + + /** + * 画面の読み込みを開始する。 + * @param obj.isMultiEngineOffMode マルチエンジンオフモードにするかどうか。無指定時はfalse扱いになる。 + * @param obj.projectFilePath 初期化時に読み込むプロジェクトファイル。無指定時は何も読み込まない。 + * @returns ロードの完了を待つPromise。 + */ + public async load(obj: { + isMultiEngineOffMode?: boolean; + projectFilePath?: string; + }) { + const win = this.getWindow(); + const firstUrl = + import.meta.env.VITE_DEV_SERVER_URL ?? "app://./index.html"; + const url = new URL(firstUrl); + url.searchParams.append( + "isMultiEngineOffMode", + (obj?.isMultiEngineOffMode ?? false).toString(), + ); + url.searchParams.append("projectFilePath", obj?.projectFilePath ?? ""); + await win.loadURL(url.toString()); + } + + public async reload(isMultiEngineOffMode: boolean | undefined) { + const win = this.getWindow(); + win.hide(); // FIXME: ダミーページ表示のほうが良い + + // 一旦適当なURLに飛ばしてページをアンロードする + await win.loadURL("about:blank"); + + log.info("Checking ENGINE status before reload app"); + const engineAndVvppController = getEngineAndVvppController(); + const engineCleanupResult = engineAndVvppController.cleanupEngines(); + + // エンジンの停止とエンジン終了後処理の待機 + if (engineCleanupResult != "alreadyCompleted") { + await engineCleanupResult; + } + log.info("Post engine kill process done. Now reloading app"); + + await engineAndVvppController.launchEngines(); + + await this.load({ + isMultiEngineOffMode: !!isMultiEngineOffMode, + }); + win.show(); + } + + public togglePinWindow() { + const win = this.getWindow(); + if (win.isAlwaysOnTop()) { + win.setAlwaysOnTop(false); + } else { + win.setAlwaysOnTop(true); + } + } + + public toggleMaximizeWindow() { + const win = this.getWindow(); + // 全画面表示中は、全画面表示解除のみを行い、最大化解除処理は実施しない + if (win.isFullScreen()) { + win.setFullScreen(false); + } else if (win.isMaximized()) { + win.unmaximize(); + } else { + win.maximize(); + } + } + + public toggleFullScreen() { + const win = this.getWindow(); + if (win.isFullScreen()) { + win.setFullScreen(false); + } else { + win.setFullScreen(true); + } + } + + public restoreAndFocus() { + const win = this.getWindow(); + if (win.isMinimized()) win.restore(); + win.focus(); + } + + public zoomIn() { + const win = this.getWindow(); + win.webContents.setZoomFactor( + Math.min(Math.max(win.webContents.getZoomFactor() + 0.1, 0.5), 3), + ); + } + + public zoomOut() { + const win = this.getWindow(); + win.webContents.setZoomFactor( + Math.min(Math.max(win.webContents.getZoomFactor() - 0.1, 0.5), 3), + ); + } + + public zoomReset() { + const win = this.getWindow(); + win.webContents.setZoomFactor(1); + } + + public destroyWindow() { + this.getWindow().destroy(); + } + + public show() { + this.getWindow().show(); + } + + public minimize() { + this.getWindow().minimize(); + } + + public isMaximized() { + return this.getWindow().isMaximized(); + } + + public showOpenDialogSync(options: OpenDialogSyncOptions) { + return this._win == undefined + ? dialog.showOpenDialogSync(options) + : dialog.showOpenDialogSync(this.getWindow(), options); + } + + public showOpenDialog(options: OpenDialogOptions) { + return this._win == undefined + ? dialog.showOpenDialog(options) + : dialog.showOpenDialog(this.getWindow(), options); + } + + public showSaveDialog(options: SaveDialogOptions) { + return this._win == undefined + ? dialog.showSaveDialog(options) + : dialog.showSaveDialog(this.getWindow(), options); + } + + public showMessageBoxSync(options: MessageBoxSyncOptions) { + return this._win == undefined + ? dialog.showMessageBoxSync(options) + : dialog.showMessageBoxSync(this.getWindow(), options); + } + + public showMessageBox(options: MessageBoxOptions) { + return this._win == undefined + ? dialog.showMessageBox(options) + : dialog.showMessageBox(this.getWindow(), options); + } +} + +let windowManager: WindowManager | undefined; + +export function initializeWindowManager(payload: WindowManagerOption) { + windowManager = new WindowManager(payload); +} + +export function getWindowManager() { + if (windowManager == undefined) { + throw new Error("WindowManager is not initialized"); + } + return windowManager; +} diff --git a/tests/e2e/electron/example.spec.ts b/tests/e2e/electron/example.spec.ts index d67508d9c0..1c0ee5dbe7 100644 --- a/tests/e2e/electron/example.spec.ts +++ b/tests/e2e/electron/example.spec.ts @@ -3,7 +3,7 @@ import os from "os"; import path from "path"; import { _electron as electron, test } from "@playwright/test"; import dotenv from "dotenv"; -import { BrowserWindow, MessageBoxSyncOptions } from "electron"; +import { MessageBoxSyncOptions } from "electron"; test.beforeAll(async () => { console.log("Waiting for main.js to be built..."); @@ -64,7 +64,6 @@ test.beforeEach(async () => { await app.evaluate((electron) => { // @ts-expect-error 2種のオーバーロードを無視する electron.dialog.showMessageBoxSync = ( - win: BrowserWindow, options: MessageBoxSyncOptions, ) => { // デフォルトエンジンのインストールの確認ダイアログ From b80b70e19e4e7e1af6d56571a66bd2d4399e47b0 Mon Sep 17 00:00:00 2001 From: Sig <62321214+sigprogramming@users.noreply.github.com> Date: Fri, 3 Jan 2025 15:18:41 +0900 Subject: [PATCH 11/23] =?UTF-8?q?OpenAPI=E3=81=AE=E5=AE=9A=E7=BE=A9?= =?UTF-8?q?=E3=82=92=E6=9B=B4=E6=96=B0=20(#2462)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Hiroshiba Kazuyuki --- openapi.json | 2 +- src/openapi/.openapi-generator/FILES | 1 + src/openapi/apis/DefaultApi.ts | 83 +++++++++++++++-- src/openapi/models/AccentPhrase.ts | 6 +- src/openapi/models/AudioQuery.ts | 4 +- src/openapi/models/BaseLibraryInfo.ts | 4 +- .../models/BodySingFrameF0SingFrameF0Post.ts | 88 +++++++++++++++++++ .../BodySingFrameVolumeSingFrameVolumePost.ts | 4 +- src/openapi/models/CorsPolicyMode.ts | 4 +- src/openapi/models/DownloadableLibraryInfo.ts | 4 +- src/openapi/models/EngineManifest.ts | 6 +- src/openapi/models/FrameAudioQuery.ts | 4 +- src/openapi/models/FramePhoneme.ts | 4 +- src/openapi/models/HTTPValidationError.ts | 4 +- src/openapi/models/InstalledLibraryInfo.ts | 4 +- src/openapi/models/LibrarySpeaker.ts | 4 +- src/openapi/models/LicenseInfo.ts | 4 +- src/openapi/models/Mora.ts | 4 +- src/openapi/models/MorphableTargetInfo.ts | 4 +- src/openapi/models/Note.ts | 4 +- src/openapi/models/ParseKanaBadRequest.ts | 4 +- src/openapi/models/Preset.ts | 4 +- src/openapi/models/Score.ts | 4 +- src/openapi/models/Speaker.ts | 6 +- src/openapi/models/SpeakerInfo.ts | 4 +- src/openapi/models/SpeakerStyle.ts | 4 +- .../models/SpeakerSupportedFeatures.ts | 4 +- src/openapi/models/StyleInfo.ts | 4 +- src/openapi/models/SupportedDevicesInfo.ts | 4 +- src/openapi/models/SupportedFeatures.ts | 4 +- src/openapi/models/UpdateInfo.ts | 4 +- src/openapi/models/UserDictWord.ts | 4 +- src/openapi/models/ValidationError.ts | 4 +- src/openapi/models/ValidationErrorLocInner.ts | 4 +- src/openapi/models/VvlibManifest.ts | 4 +- src/openapi/models/WordTypes.ts | 4 +- src/openapi/models/index.ts | 1 + src/openapi/runtime.ts | 4 +- 38 files changed, 237 insertions(+), 76 deletions(-) create mode 100644 src/openapi/models/BodySingFrameF0SingFrameF0Post.ts diff --git a/openapi.json b/openapi.json index b91b38e738..950712c050 100644 --- a/openapi.json +++ b/openapi.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"VOICEVOX Engine","description":"VOICEVOX の音声合成エンジンです。","version":"latest"},"paths":{"/audio_query":{"post":{"tags":["クエリ作成"],"summary":"音声合成用のクエリを作成する","description":"音声合成用のクエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。","operationId":"audio_query_audio_query_post","parameters":[{"name":"text","in":"query","required":true,"schema":{"type":"string","title":"Text"}},{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/audio_query_from_preset":{"post":{"tags":["クエリ作成"],"summary":"音声合成用のクエリをプリセットを用いて作成する","description":"音声合成用のクエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。","operationId":"audio_query_from_preset_audio_query_from_preset_post","parameters":[{"name":"text","in":"query","required":true,"schema":{"type":"string","title":"Text"}},{"name":"preset_id","in":"query","required":true,"schema":{"type":"integer","title":"Preset Id"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/accent_phrases":{"post":{"tags":["クエリ編集"],"summary":"テキストからアクセント句を得る","description":"テキストからアクセント句を得ます。\nis_kanaが`true`のとき、テキストは次のAquesTalk 風記法で解釈されます。デフォルトは`false`です。\n* 全てのカナはカタカナで記述される\n* アクセント句は`/`または`、`で区切る。`、`で区切った場合に限り無音区間が挿入される。\n* カナの手前に`_`を入れるとそのカナは無声化される\n* アクセント位置を`'`で指定する。全てのアクセント句にはアクセント位置を1つ指定する必要がある。\n* アクセント句末に`?`(全角)を入れることにより疑問文の発音ができる。","operationId":"accent_phrases_accent_phrases_post","parameters":[{"name":"text","in":"query","required":true,"schema":{"type":"string","title":"Text"}},{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"is_kana","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Is Kana"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Response Accent Phrases Accent Phrases Post"}}}},"400":{"description":"読み仮名のパースに失敗","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParseKanaBadRequest"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mora_data":{"post":{"tags":["クエリ編集"],"summary":"アクセント句から音高・音素長を得る","operationId":"mora_data_mora_data_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Accent Phrases"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Response Mora Data Mora Data Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mora_length":{"post":{"tags":["クエリ編集"],"summary":"アクセント句から音素長を得る","operationId":"mora_length_mora_length_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Accent Phrases"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Response Mora Length Mora Length Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mora_pitch":{"post":{"tags":["クエリ編集"],"summary":"アクセント句から音高を得る","operationId":"mora_pitch_mora_pitch_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Accent Phrases"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Response Mora Pitch Mora Pitch Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/synthesis":{"post":{"tags":["音声合成"],"summary":"音声合成する","operationId":"synthesis_synthesis_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"enable_interrogative_upspeak","in":"query","required":false,"schema":{"type":"boolean","description":"疑問系のテキストが与えられたら語尾を自動調整する","default":true,"title":"Enable Interrogative Upspeak"},"description":"疑問系のテキストが与えられたら語尾を自動調整する"},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}}},"responses":{"200":{"description":"Successful Response","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/cancellable_synthesis":{"post":{"tags":["音声合成"],"summary":"音声合成する(キャンセル可能)","operationId":"cancellable_synthesis_cancellable_synthesis_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}}},"responses":{"200":{"description":"Successful Response","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/multi_synthesis":{"post":{"tags":["音声合成"],"summary":"複数まとめて音声合成する","operationId":"multi_synthesis_multi_synthesis_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AudioQuery"},"title":"Queries"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sing_frame_audio_query":{"post":{"tags":["クエリ作成"],"summary":"歌唱音声合成用のクエリを作成する","description":"歌唱音声合成用のクエリの初期値を得ます。ここで得られたクエリはそのまま歌唱音声合成に利用できます。各値の意味は`Schemas`を参照してください。","operationId":"sing_frame_audio_query_sing_frame_audio_query_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Score"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FrameAudioQuery"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sing_frame_volume":{"post":{"tags":["クエリ編集"],"summary":"スコア・歌唱音声合成用のクエリからフレームごとの音量を得る","operationId":"sing_frame_volume_sing_frame_volume_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_sing_frame_volume_sing_frame_volume_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"number"},"title":"Response Sing Frame Volume Sing Frame Volume Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/frame_synthesis":{"post":{"tags":["音声合成"],"summary":"Frame Synthesis","description":"歌唱音声合成を行います。","operationId":"frame_synthesis_frame_synthesis_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FrameAudioQuery"}}}},"responses":{"200":{"description":"Successful Response","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/connect_waves":{"post":{"tags":["その他"],"summary":"base64エンコードされた複数のwavデータを一つに結合する","description":"base64エンコードされたwavデータを一纏めにし、wavファイルで返します。","operationId":"connect_waves_connect_waves_post","requestBody":{"content":{"application/json":{"schema":{"items":{"type":"string"},"type":"array","title":"Waves"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/validate_kana":{"post":{"tags":["その他"],"summary":"テキストがAquesTalk 風記法に従っているか判定する","description":"テキストがAquesTalk 風記法に従っているかどうかを判定します。\n従っていない場合はエラーが返ります。","operationId":"validate_kana_validate_kana_post","parameters":[{"name":"text","in":"query","required":true,"schema":{"type":"string","description":"判定する対象の文字列","title":"Text"},"description":"判定する対象の文字列"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"boolean","title":"Response Validate Kana Validate Kana Post"}}}},"400":{"description":"テキストが不正です","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParseKanaBadRequest"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/initialize_speaker":{"post":{"tags":["その他"],"summary":"Initialize Speaker","description":"指定されたスタイルを初期化します。\n実行しなくても他のAPIは使用できますが、初回実行時に時間がかかることがあります。","operationId":"initialize_speaker_initialize_speaker_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"skip_reinit","in":"query","required":false,"schema":{"type":"boolean","description":"既に初期化済みのスタイルの再初期化をスキップするかどうか","default":false,"title":"Skip Reinit"},"description":"既に初期化済みのスタイルの再初期化をスキップするかどうか"},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/is_initialized_speaker":{"get":{"tags":["その他"],"summary":"Is Initialized Speaker","description":"指定されたスタイルが初期化されているかどうかを返します。","operationId":"is_initialized_speaker_is_initialized_speaker_get","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"boolean","title":"Response Is Initialized Speaker Is Initialized Speaker Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/supported_devices":{"get":{"tags":["その他"],"summary":"Supported Devices","description":"対応デバイスの一覧を取得します。","operationId":"supported_devices_supported_devices_get","parameters":[{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportedDevicesInfo"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/morphable_targets":{"post":{"tags":["音声合成"],"summary":"指定したスタイルに対してエンジン内のキャラクターがモーフィングが可能か判定する","description":"指定されたベーススタイルに対してエンジン内の各キャラクターがモーフィング機能を利用可能か返します。\nモーフィングの許可/禁止は`/speakers`の`speaker.supported_features.synthesis_morphing`に記載されています。\nプロパティが存在しない場合は、モーフィングが許可されているとみなします。\n返り値のスタイルIDはstring型なので注意。","operationId":"morphable_targets_morphable_targets_post","parameters":[{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"type":"integer"},"title":"Base Style Ids"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/MorphableTargetInfo"}},"title":"Response Morphable Targets Morphable Targets Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/synthesis_morphing":{"post":{"tags":["音声合成"],"summary":"2種類のスタイルでモーフィングした音声を合成する","description":"指定された2種類のスタイルで音声を合成、指定した割合でモーフィングした音声を得ます。\nモーフィングの割合は`morph_rate`で指定でき、0.0でベースのスタイル、1.0でターゲットのスタイルに近づきます。","operationId":"_synthesis_morphing_synthesis_morphing_post","parameters":[{"name":"base_speaker","in":"query","required":true,"schema":{"type":"integer","title":"Base Speaker"}},{"name":"target_speaker","in":"query","required":true,"schema":{"type":"integer","title":"Target Speaker"}},{"name":"morph_rate","in":"query","required":true,"schema":{"type":"number","maximum":1.0,"minimum":0.0,"title":"Morph Rate"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}}},"responses":{"200":{"description":"Successful Response","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/presets":{"get":{"tags":["その他"],"summary":"Get Presets","description":"エンジンが保持しているプリセットの設定を返します","operationId":"get_presets_presets_get","responses":{"200":{"description":"プリセットのリスト","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Preset"},"type":"array","title":"Response Get Presets Presets Get"}}}}}}},"/add_preset":{"post":{"tags":["その他"],"summary":"Add Preset","description":"新しいプリセットを追加します","operationId":"add_preset_add_preset_post","requestBody":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Preset"}],"title":"Preset","description":"新しいプリセット。プリセットIDが既存のものと重複している場合は、新規のプリセットIDが採番されます。"}}},"required":true},"responses":{"200":{"description":"追加したプリセットのプリセットID","content":{"application/json":{"schema":{"type":"integer","title":"Response Add Preset Add Preset Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/update_preset":{"post":{"tags":["その他"],"summary":"Update Preset","description":"既存のプリセットを更新します","operationId":"update_preset_update_preset_post","requestBody":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Preset"}],"title":"Preset","description":"更新するプリセット。プリセットIDが更新対象と一致している必要があります。"}}},"required":true},"responses":{"200":{"description":"更新したプリセットのプリセットID","content":{"application/json":{"schema":{"type":"integer","title":"Response Update Preset Update Preset Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/delete_preset":{"post":{"tags":["その他"],"summary":"Delete Preset","description":"既存のプリセットを削除します","operationId":"delete_preset_delete_preset_post","parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"integer","description":"削除するプリセットのプリセットID","title":"Id"},"description":"削除するプリセットのプリセットID"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/speakers":{"get":{"tags":["その他"],"summary":"Speakers","description":"喋れるキャラクターの情報の一覧を返します。","operationId":"speakers_speakers_get","parameters":[{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Speaker"},"title":"Response Speakers Speakers Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/speaker_info":{"get":{"tags":["その他"],"summary":"Speaker Info","description":"UUID で指定された喋れるキャラクターの情報を返します。\n画像や音声はresource_formatで指定した形式で返されます。","operationId":"speaker_info_speaker_info_get","parameters":[{"name":"speaker_uuid","in":"query","required":true,"schema":{"type":"string","title":"Speaker Uuid"}},{"name":"resource_format","in":"query","required":false,"schema":{"enum":["base64","url"],"type":"string","default":"base64","title":"Resource Format"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpeakerInfo"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/singers":{"get":{"tags":["その他"],"summary":"Singers","description":"歌えるキャラクターの情報の一覧を返します。","operationId":"singers_singers_get","parameters":[{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Speaker"},"title":"Response Singers Singers Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/singer_info":{"get":{"tags":["その他"],"summary":"Singer Info","description":"UUID で指定された歌えるキャラクターの情報を返します。\n画像や音声はresource_formatで指定した形式で返されます。","operationId":"singer_info_singer_info_get","parameters":[{"name":"speaker_uuid","in":"query","required":true,"schema":{"type":"string","title":"Speaker Uuid"}},{"name":"resource_format","in":"query","required":false,"schema":{"enum":["base64","url"],"type":"string","default":"base64","title":"Resource Format"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpeakerInfo"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/downloadable_libraries":{"get":{"tags":["音声ライブラリ管理"],"summary":"Downloadable Libraries","description":"ダウンロード可能な音声ライブラリの情報を返します。","operationId":"downloadable_libraries_downloadable_libraries_get","responses":{"200":{"description":"ダウンロード可能な音声ライブラリの情報リスト","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/DownloadableLibraryInfo"},"type":"array","title":"Response Downloadable Libraries Downloadable Libraries Get"}}}}}}},"/installed_libraries":{"get":{"tags":["音声ライブラリ管理"],"summary":"Installed Libraries","description":"インストールした音声ライブラリの情報を返します。","operationId":"installed_libraries_installed_libraries_get","responses":{"200":{"description":"インストールした音声ライブラリの情報","content":{"application/json":{"schema":{"additionalProperties":{"$ref":"#/components/schemas/InstalledLibraryInfo"},"type":"object","title":"Response Installed Libraries Installed Libraries Get"}}}}}}},"/install_library/{library_uuid}":{"post":{"tags":["音声ライブラリ管理"],"summary":"Install Library","description":"音声ライブラリをインストールします。\n音声ライブラリのZIPファイルをリクエストボディとして送信してください。","operationId":"install_library_install_library__library_uuid__post","parameters":[{"name":"library_uuid","in":"path","required":true,"schema":{"type":"string","description":"音声ライブラリのID","title":"Library Uuid"},"description":"音声ライブラリのID"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/uninstall_library/{library_uuid}":{"post":{"tags":["音声ライブラリ管理"],"summary":"Uninstall Library","description":"音声ライブラリをアンインストールします。","operationId":"uninstall_library_uninstall_library__library_uuid__post","parameters":[{"name":"library_uuid","in":"path","required":true,"schema":{"type":"string","description":"音声ライブラリのID","title":"Library Uuid"},"description":"音声ライブラリのID"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user_dict":{"get":{"tags":["ユーザー辞書"],"summary":"Get User Dict Words","description":"ユーザー辞書に登録されている単語の一覧を返します。\n単語の表層形(surface)は正規化済みの物を返します。","operationId":"get_user_dict_words_user_dict_get","responses":{"200":{"description":"単語のUUIDとその詳細","content":{"application/json":{"schema":{"additionalProperties":{"$ref":"#/components/schemas/UserDictWord"},"type":"object","title":"Response Get User Dict Words User Dict Get"}}}}}}},"/user_dict_word":{"post":{"tags":["ユーザー辞書"],"summary":"Add User Dict Word","description":"ユーザー辞書に言葉を追加します。","operationId":"add_user_dict_word_user_dict_word_post","parameters":[{"name":"surface","in":"query","required":true,"schema":{"type":"string","description":"言葉の表層形","title":"Surface"},"description":"言葉の表層形"},{"name":"pronunciation","in":"query","required":true,"schema":{"type":"string","description":"言葉の発音(カタカナ)","title":"Pronunciation"},"description":"言葉の発音(カタカナ)"},{"name":"accent_type","in":"query","required":true,"schema":{"type":"integer","description":"アクセント型(音が下がる場所を指す)","title":"Accent Type"},"description":"アクセント型(音が下がる場所を指す)"},{"name":"word_type","in":"query","required":false,"schema":{"allOf":[{"$ref":"#/components/schemas/WordTypes"}],"description":"PROPER_NOUN(固有名詞)、COMMON_NOUN(普通名詞)、VERB(動詞)、ADJECTIVE(形容詞)、SUFFIX(語尾)のいずれか","title":"Word Type"},"description":"PROPER_NOUN(固有名詞)、COMMON_NOUN(普通名詞)、VERB(動詞)、ADJECTIVE(形容詞)、SUFFIX(語尾)のいずれか"},{"name":"priority","in":"query","required":false,"schema":{"type":"integer","description":"単語の優先度(0から10までの整数)。数字が大きいほど優先度が高くなる。1から9までの値を指定することを推奨","maximum":10,"minimum":0,"title":"Priority"},"description":"単語の優先度(0から10までの整数)。数字が大きいほど優先度が高くなる。1から9までの値を指定することを推奨"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"string","title":"Response Add User Dict Word User Dict Word Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user_dict_word/{word_uuid}":{"put":{"tags":["ユーザー辞書"],"summary":"Rewrite User Dict Word","description":"ユーザー辞書に登録されている言葉を更新します。","operationId":"rewrite_user_dict_word_user_dict_word__word_uuid__put","parameters":[{"name":"word_uuid","in":"path","required":true,"schema":{"type":"string","description":"更新する言葉のUUID","title":"Word Uuid"},"description":"更新する言葉のUUID"},{"name":"surface","in":"query","required":true,"schema":{"type":"string","description":"言葉の表層形","title":"Surface"},"description":"言葉の表層形"},{"name":"pronunciation","in":"query","required":true,"schema":{"type":"string","description":"言葉の発音(カタカナ)","title":"Pronunciation"},"description":"言葉の発音(カタカナ)"},{"name":"accent_type","in":"query","required":true,"schema":{"type":"integer","description":"アクセント型(音が下がる場所を指す)","title":"Accent Type"},"description":"アクセント型(音が下がる場所を指す)"},{"name":"word_type","in":"query","required":false,"schema":{"allOf":[{"$ref":"#/components/schemas/WordTypes"}],"description":"PROPER_NOUN(固有名詞)、COMMON_NOUN(普通名詞)、VERB(動詞)、ADJECTIVE(形容詞)、SUFFIX(語尾)のいずれか","title":"Word Type"},"description":"PROPER_NOUN(固有名詞)、COMMON_NOUN(普通名詞)、VERB(動詞)、ADJECTIVE(形容詞)、SUFFIX(語尾)のいずれか"},{"name":"priority","in":"query","required":false,"schema":{"type":"integer","description":"単語の優先度(0から10までの整数)。数字が大きいほど優先度が高くなる。1から9までの値を指定することを推奨。","maximum":10,"minimum":0,"title":"Priority"},"description":"単語の優先度(0から10までの整数)。数字が大きいほど優先度が高くなる。1から9までの値を指定することを推奨。"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["ユーザー辞書"],"summary":"Delete User Dict Word","description":"ユーザー辞書に登録されている言葉を削除します。","operationId":"delete_user_dict_word_user_dict_word__word_uuid__delete","parameters":[{"name":"word_uuid","in":"path","required":true,"schema":{"type":"string","description":"削除する言葉のUUID","title":"Word Uuid"},"description":"削除する言葉のUUID"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/import_user_dict":{"post":{"tags":["ユーザー辞書"],"summary":"Import User Dict Words","description":"他のユーザー辞書をインポートします。","operationId":"import_user_dict_words_import_user_dict_post","parameters":[{"name":"override","in":"query","required":true,"schema":{"type":"boolean","description":"重複したエントリがあった場合、上書きするかどうか","title":"Override"},"description":"重複したエントリがあった場合、上書きするかどうか"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/UserDictWord"},"description":"インポートするユーザー辞書のデータ","title":"Import Dict Data"}}}},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/version":{"get":{"tags":["その他"],"summary":"Version","description":"エンジンのバージョンを取得します。","operationId":"version_version_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"string","title":"Response Version Version Get"}}}}}}},"/core_versions":{"get":{"tags":["その他"],"summary":"Core Versions","description":"利用可能なコアのバージョン一覧を取得します。","operationId":"core_versions_core_versions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"type":"string"},"type":"array","title":"Response Core Versions Core Versions Get"}}}}}}},"/engine_manifest":{"get":{"tags":["その他"],"summary":"Engine Manifest","description":"エンジンマニフェストを取得します。","operationId":"engine_manifest_engine_manifest_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EngineManifest"}}}}}}},"/setting":{"get":{"tags":["設定"],"summary":"Setting Get","description":"設定ページを返します。","operationId":"setting_get_setting_get","responses":{"200":{"description":"Successful Response"}}},"post":{"tags":["設定"],"summary":"Setting Post","description":"設定を更新します。","operationId":"setting_post_setting_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_setting_post_setting_post"}}},"required":true},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"tags":["その他"],"summary":"Get Portal Page","description":"ポータルページを返します。","operationId":"get_portal_page__get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}}},"components":{"schemas":{"AccentPhrase":{"properties":{"moras":{"items":{"$ref":"#/components/schemas/Mora"},"type":"array","title":"Moras","description":"モーラのリスト"},"accent":{"type":"integer","title":"Accent","description":"アクセント箇所"},"pause_mora":{"allOf":[{"$ref":"#/components/schemas/Mora"}],"title":"Pause Mora","description":"後ろに無音を付けるかどうか"},"is_interrogative":{"type":"boolean","title":"Is Interrogative","description":"疑問系かどうか","default":false}},"type":"object","required":["moras","accent"],"title":"AccentPhrase","description":"アクセント句ごとの情報"},"AudioQuery":{"properties":{"accent_phrases":{"items":{"$ref":"#/components/schemas/AccentPhrase"},"type":"array","title":"Accent Phrases","description":"アクセント句のリスト"},"speedScale":{"type":"number","title":"Speedscale","description":"全体の話速"},"pitchScale":{"type":"number","title":"Pitchscale","description":"全体の音高"},"intonationScale":{"type":"number","title":"Intonationscale","description":"全体の抑揚"},"volumeScale":{"type":"number","title":"Volumescale","description":"全体の音量"},"prePhonemeLength":{"type":"number","title":"Prephonemelength","description":"音声の前の無音時間"},"postPhonemeLength":{"type":"number","title":"Postphonemelength","description":"音声の後の無音時間"},"pauseLength":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Pauselength","description":"句読点などの無音時間。nullのときは無視される。デフォルト値はnull"},"pauseLengthScale":{"type":"number","title":"Pauselengthscale","description":"句読点などの無音時間(倍率)。デフォルト値は1","default":1},"outputSamplingRate":{"type":"integer","title":"Outputsamplingrate","description":"音声データの出力サンプリングレート"},"outputStereo":{"type":"boolean","title":"Outputstereo","description":"音声データをステレオ出力するか否か"},"kana":{"type":"string","title":"Kana","description":"[読み取り専用]AquesTalk 風記法によるテキスト。音声合成用のクエリとしては無視される"}},"type":"object","required":["accent_phrases","speedScale","pitchScale","intonationScale","volumeScale","prePhonemeLength","postPhonemeLength","outputSamplingRate","outputStereo"],"title":"AudioQuery","description":"音声合成用のクエリ"},"Body_setting_post_setting_post":{"properties":{"cors_policy_mode":{"$ref":"#/components/schemas/CorsPolicyMode"},"allow_origin":{"type":"string","title":"Allow Origin"}},"type":"object","required":["cors_policy_mode"],"title":"Body_setting_post_setting_post"},"Body_sing_frame_volume_sing_frame_volume_post":{"properties":{"score":{"$ref":"#/components/schemas/Score"},"frame_audio_query":{"$ref":"#/components/schemas/FrameAudioQuery"}},"type":"object","required":["score","frame_audio_query"],"title":"Body_sing_frame_volume_sing_frame_volume_post"},"CorsPolicyMode":{"type":"string","enum":["all","localapps"],"title":"CorsPolicyMode","description":"CORSの許可モード"},"DownloadableLibraryInfo":{"properties":{"name":{"type":"string","title":"Name","description":"音声ライブラリの名前"},"uuid":{"type":"string","title":"Uuid","description":"音声ライブラリのUUID"},"version":{"type":"string","title":"Version","description":"音声ライブラリのバージョン"},"download_url":{"type":"string","title":"Download Url","description":"音声ライブラリのダウンロードURL"},"bytes":{"type":"integer","title":"Bytes","description":"音声ライブラリのバイト数"},"speakers":{"items":{"$ref":"#/components/schemas/LibrarySpeaker"},"type":"array","title":"Speakers","description":"音声ライブラリに含まれるキャラクターのリスト"}},"type":"object","required":["name","uuid","version","download_url","bytes","speakers"],"title":"DownloadableLibraryInfo","description":"ダウンロード可能な音声ライブラリの情報"},"EngineManifest":{"properties":{"manifest_version":{"type":"string","title":"Manifest Version","description":"マニフェストのバージョン"},"name":{"type":"string","title":"Name","description":"エンジン名"},"brand_name":{"type":"string","title":"Brand Name","description":"ブランド名"},"uuid":{"type":"string","title":"Uuid","description":"エンジンのUUID"},"url":{"type":"string","title":"Url","description":"エンジンのURL"},"icon":{"type":"string","title":"Icon","description":"エンジンのアイコンをBASE64エンコードしたもの"},"default_sampling_rate":{"type":"integer","title":"Default Sampling Rate","description":"デフォルトのサンプリング周波数"},"frame_rate":{"type":"number","title":"Frame Rate","description":"エンジンのフレームレート"},"terms_of_service":{"type":"string","title":"Terms Of Service","description":"エンジンの利用規約"},"update_infos":{"items":{"$ref":"#/components/schemas/UpdateInfo"},"type":"array","title":"Update Infos","description":"エンジンのアップデート情報"},"dependency_licenses":{"items":{"$ref":"#/components/schemas/LicenseInfo"},"type":"array","title":"Dependency Licenses","description":"依存関係のライセンス情報"},"supported_vvlib_manifest_version":{"type":"string","title":"Supported Vvlib Manifest Version","description":"エンジンが対応するvvlibのバージョン"},"supported_features":{"allOf":[{"$ref":"#/components/schemas/SupportedFeatures"}],"description":"エンジンが持つ機能"}},"type":"object","required":["manifest_version","name","brand_name","uuid","url","icon","default_sampling_rate","frame_rate","terms_of_service","update_infos","dependency_licenses","supported_features"],"title":"EngineManifest","description":"エンジン自体に関する情報"},"FrameAudioQuery":{"properties":{"f0":{"items":{"type":"number"},"type":"array","title":"F0","description":"フレームごとの基本周波数"},"volume":{"items":{"type":"number"},"type":"array","title":"Volume","description":"フレームごとの音量"},"phonemes":{"items":{"$ref":"#/components/schemas/FramePhoneme"},"type":"array","title":"Phonemes","description":"音素のリスト"},"volumeScale":{"type":"number","title":"Volumescale","description":"全体の音量"},"outputSamplingRate":{"type":"integer","title":"Outputsamplingrate","description":"音声データの出力サンプリングレート"},"outputStereo":{"type":"boolean","title":"Outputstereo","description":"音声データをステレオ出力するか否か"}},"type":"object","required":["f0","volume","phonemes","volumeScale","outputSamplingRate","outputStereo"],"title":"FrameAudioQuery","description":"フレームごとの音声合成用のクエリ"},"FramePhoneme":{"properties":{"phoneme":{"type":"string","title":"Phoneme","description":"音素"},"frame_length":{"type":"integer","title":"Frame Length","description":"音素のフレーム長"},"note_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note Id","description":"音符のID"}},"type":"object","required":["phoneme","frame_length"],"title":"FramePhoneme","description":"音素の情報"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"InstalledLibraryInfo":{"properties":{"name":{"type":"string","title":"Name","description":"音声ライブラリの名前"},"uuid":{"type":"string","title":"Uuid","description":"音声ライブラリのUUID"},"version":{"type":"string","title":"Version","description":"音声ライブラリのバージョン"},"download_url":{"type":"string","title":"Download Url","description":"音声ライブラリのダウンロードURL"},"bytes":{"type":"integer","title":"Bytes","description":"音声ライブラリのバイト数"},"speakers":{"items":{"$ref":"#/components/schemas/LibrarySpeaker"},"type":"array","title":"Speakers","description":"音声ライブラリに含まれるキャラクターのリスト"},"uninstallable":{"type":"boolean","title":"Uninstallable","description":"アンインストール可能かどうか"}},"type":"object","required":["name","uuid","version","download_url","bytes","speakers","uninstallable"],"title":"InstalledLibraryInfo","description":"インストール済み音声ライブラリの情報"},"LibrarySpeaker":{"properties":{"speaker":{"$ref":"#/components/schemas/Speaker"},"speaker_info":{"$ref":"#/components/schemas/SpeakerInfo"}},"type":"object","required":["speaker","speaker_info"],"title":"LibrarySpeaker","description":"音声ライブラリに含まれるキャラクターの情報"},"LicenseInfo":{"properties":{"name":{"type":"string","title":"Name","description":"依存ライブラリ名"},"version":{"type":"string","title":"Version","description":"依存ライブラリのバージョン"},"license":{"type":"string","title":"License","description":"依存ライブラリのライセンス名"},"text":{"type":"string","title":"Text","description":"依存ライブラリのライセンス本文"}},"type":"object","required":["name","text"],"title":"LicenseInfo","description":"依存ライブラリのライセンス情報"},"Mora":{"properties":{"text":{"type":"string","title":"Text","description":"文字"},"consonant":{"type":"string","title":"Consonant","description":"子音の音素"},"consonant_length":{"type":"number","title":"Consonant Length","description":"子音の音長"},"vowel":{"type":"string","title":"Vowel","description":"母音の音素"},"vowel_length":{"type":"number","title":"Vowel Length","description":"母音の音長"},"pitch":{"type":"number","title":"Pitch","description":"音高"}},"type":"object","required":["text","vowel","vowel_length","pitch"],"title":"Mora","description":"モーラ(子音+母音)ごとの情報"},"MorphableTargetInfo":{"properties":{"is_morphable":{"type":"boolean","title":"Is Morphable","description":"指定したキャラクターに対してモーフィングの可否"}},"type":"object","required":["is_morphable"],"title":"MorphableTargetInfo"},"Note":{"properties":{"id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Id","description":"ID"},"key":{"type":"integer","title":"Key","description":"音階"},"frame_length":{"type":"integer","title":"Frame Length","description":"音符のフレーム長"},"lyric":{"type":"string","title":"Lyric","description":"音符の歌詞"}},"type":"object","required":["frame_length","lyric"],"title":"Note","description":"音符ごとの情報"},"ParseKanaBadRequest":{"properties":{"text":{"type":"string","title":"Text","description":"エラーメッセージ"},"error_name":{"type":"string","title":"Error Name","description":"エラー名\n\n|name|description|\n|---|---|\n| UNKNOWN_TEXT | 判別できない読み仮名があります: {text} |\n| ACCENT_TOP | 句頭にアクセントは置けません: {text} |\n| ACCENT_TWICE | 1つのアクセント句に二つ以上のアクセントは置けません: {text} |\n| ACCENT_NOTFOUND | アクセントを指定していないアクセント句があります: {text} |\n| EMPTY_PHRASE | {position}番目のアクセント句が空白です |\n| INTERROGATION_MARK_NOT_AT_END | アクセント句末以外に「?」は置けません: {text} |\n| INFINITE_LOOP | 処理時に無限ループになってしまいました...バグ報告をお願いします。 |"},"error_args":{"additionalProperties":{"type":"string"},"type":"object","title":"Error Args","description":"エラーを起こした箇所"}},"type":"object","required":["text","error_name","error_args"],"title":"ParseKanaBadRequest"},"Preset":{"properties":{"id":{"type":"integer","title":"Id","description":"プリセットID"},"name":{"type":"string","title":"Name","description":"プリセット名"},"speaker_uuid":{"type":"string","title":"Speaker Uuid","description":"キャラクターのUUID"},"style_id":{"type":"integer","title":"Style Id","description":"スタイルID"},"speedScale":{"type":"number","title":"Speedscale","description":"全体の話速"},"pitchScale":{"type":"number","title":"Pitchscale","description":"全体の音高"},"intonationScale":{"type":"number","title":"Intonationscale","description":"全体の抑揚"},"volumeScale":{"type":"number","title":"Volumescale","description":"全体の音量"},"prePhonemeLength":{"type":"number","title":"Prephonemelength","description":"音声の前の無音時間"},"postPhonemeLength":{"type":"number","title":"Postphonemelength","description":"音声の後の無音時間"},"pauseLength":{"type":"number","title":"Pauselength","description":"句読点などの無音時間"},"pauseLengthScale":{"type":"number","title":"Pauselengthscale","description":"句読点などの無音時間(倍率)","default":1}},"type":"object","required":["id","name","speaker_uuid","style_id","speedScale","pitchScale","intonationScale","volumeScale","prePhonemeLength","postPhonemeLength"],"title":"Preset","description":"プリセット情報"},"Score":{"properties":{"notes":{"items":{"$ref":"#/components/schemas/Note"},"type":"array","title":"Notes","description":"音符のリスト"}},"type":"object","required":["notes"],"title":"Score","description":"楽譜情報"},"Speaker":{"properties":{"name":{"type":"string","title":"Name","description":"名前"},"speaker_uuid":{"type":"string","title":"Speaker Uuid","description":"キャラクターのUUID"},"styles":{"items":{"$ref":"#/components/schemas/SpeakerStyle"},"type":"array","title":"Styles","description":"スタイルの一覧"},"version":{"type":"string","title":"Version","description":"キャラクターのバージョン"},"supported_features":{"allOf":[{"$ref":"#/components/schemas/SpeakerSupportedFeatures"}],"description":"キャラクターの対応機能"}},"type":"object","required":["name","speaker_uuid","styles","version"],"title":"Speaker","description":"キャラクター情報"},"SpeakerInfo":{"properties":{"policy":{"type":"string","title":"Policy","description":"policy.md"},"portrait":{"type":"string","title":"Portrait","description":"立ち絵画像をbase64エンコードしたもの、あるいはURL"},"style_infos":{"items":{"$ref":"#/components/schemas/StyleInfo"},"type":"array","title":"Style Infos","description":"スタイルの追加情報"}},"type":"object","required":["policy","portrait","style_infos"],"title":"SpeakerInfo","description":"キャラクターの追加情報"},"SpeakerStyle":{"properties":{"name":{"type":"string","title":"Name","description":"スタイル名"},"id":{"type":"integer","title":"Id","description":"スタイルID"},"type":{"type":"string","enum":["talk","singing_teacher","frame_decode","sing"],"title":"Type","description":"スタイルの種類。talk:音声合成クエリの作成と音声合成が可能。singing_teacher:歌唱音声合成用のクエリの作成が可能。frame_decode:歌唱音声合成が可能。sing:歌唱音声合成用のクエリの作成と歌唱音声合成が可能。","default":"talk"}},"type":"object","required":["name","id"],"title":"SpeakerStyle","description":"キャラクターのスタイル情報"},"SpeakerSupportedFeatures":{"properties":{"permitted_synthesis_morphing":{"type":"string","enum":["ALL","SELF_ONLY","NOTHING"],"title":"Permitted Synthesis Morphing","description":"モーフィング機能への対応。'ALL' は「全て許可」、'SELF_ONLY' は「同じキャラクター内でのみ許可」、'NOTHING' は「全て禁止」","default":"ALL"}},"type":"object","title":"SpeakerSupportedFeatures","description":"キャラクターの対応機能の情報"},"StyleInfo":{"properties":{"id":{"type":"integer","title":"Id","description":"スタイルID"},"icon":{"type":"string","title":"Icon","description":"このスタイルのアイコンをbase64エンコードしたもの、あるいはURL"},"portrait":{"type":"string","title":"Portrait","description":"このスタイルの立ち絵画像をbase64エンコードしたもの、あるいはURL"},"voice_samples":{"items":{"type":"string"},"type":"array","title":"Voice Samples","description":"サンプル音声をbase64エンコードしたもの、あるいはURL"}},"type":"object","required":["id","icon","voice_samples"],"title":"StyleInfo","description":"スタイルの追加情報"},"SupportedDevicesInfo":{"properties":{"cpu":{"type":"boolean","title":"Cpu","description":"CPUに対応しているか"},"cuda":{"type":"boolean","title":"Cuda","description":"CUDA(Nvidia GPU)に対応しているか"},"dml":{"type":"boolean","title":"Dml","description":"DirectML(Nvidia GPU/Radeon GPU等)に対応しているか"}},"type":"object","required":["cpu","cuda","dml"],"title":"SupportedDevicesInfo","description":"対応しているデバイスの情報"},"SupportedFeatures":{"properties":{"adjust_mora_pitch":{"type":"boolean","title":"Adjust Mora Pitch","description":"モーラごとの音高の調整"},"adjust_phoneme_length":{"type":"boolean","title":"Adjust Phoneme Length","description":"音素ごとの長さの調整"},"adjust_speed_scale":{"type":"boolean","title":"Adjust Speed Scale","description":"全体の話速の調整"},"adjust_pitch_scale":{"type":"boolean","title":"Adjust Pitch Scale","description":"全体の音高の調整"},"adjust_intonation_scale":{"type":"boolean","title":"Adjust Intonation Scale","description":"全体の抑揚の調整"},"adjust_volume_scale":{"type":"boolean","title":"Adjust Volume Scale","description":"全体の音量の調整"},"adjust_pause_length":{"type":"boolean","title":"Adjust Pause Length","description":"句読点などの無音時間の調整"},"interrogative_upspeak":{"type":"boolean","title":"Interrogative Upspeak","description":"疑問文の自動調整"},"synthesis_morphing":{"type":"boolean","title":"Synthesis Morphing","description":"2種類のスタイルでモーフィングした音声を合成"},"sing":{"type":"boolean","title":"Sing","description":"歌唱音声合成"},"manage_library":{"type":"boolean","title":"Manage Library","description":"音声ライブラリのインストール・アンインストール"},"return_resource_url":{"type":"boolean","title":"Return Resource Url","description":"キャラクター情報のリソースをURLで返送"}},"type":"object","required":["adjust_mora_pitch","adjust_phoneme_length","adjust_speed_scale","adjust_pitch_scale","adjust_intonation_scale","adjust_volume_scale","interrogative_upspeak","synthesis_morphing"],"title":"SupportedFeatures","description":"エンジンが持つ機能の一覧"},"UpdateInfo":{"properties":{"version":{"type":"string","title":"Version","description":"エンジンのバージョン名"},"descriptions":{"items":{"type":"string"},"type":"array","title":"Descriptions","description":"アップデートの詳細についての説明"},"contributors":{"items":{"type":"string"},"type":"array","title":"Contributors","description":"貢献者名"}},"type":"object","required":["version","descriptions"],"title":"UpdateInfo","description":"エンジンのアップデート情報"},"UserDictWord":{"properties":{"surface":{"type":"string","title":"Surface","description":"表層形"},"priority":{"type":"integer","maximum":10.0,"minimum":0.0,"title":"Priority","description":"優先度"},"context_id":{"type":"integer","title":"Context Id","description":"文脈ID","default":1348},"part_of_speech":{"type":"string","title":"Part Of Speech","description":"品詞"},"part_of_speech_detail_1":{"type":"string","title":"Part Of Speech Detail 1","description":"品詞細分類1"},"part_of_speech_detail_2":{"type":"string","title":"Part Of Speech Detail 2","description":"品詞細分類2"},"part_of_speech_detail_3":{"type":"string","title":"Part Of Speech Detail 3","description":"品詞細分類3"},"inflectional_type":{"type":"string","title":"Inflectional Type","description":"活用型"},"inflectional_form":{"type":"string","title":"Inflectional Form","description":"活用形"},"stem":{"type":"string","title":"Stem","description":"原形"},"yomi":{"type":"string","title":"Yomi","description":"読み"},"pronunciation":{"type":"string","title":"Pronunciation","description":"発音"},"accent_type":{"type":"integer","title":"Accent Type","description":"アクセント型"},"mora_count":{"type":"integer","title":"Mora Count","description":"モーラ数"},"accent_associative_rule":{"type":"string","title":"Accent Associative Rule","description":"アクセント結合規則"}},"type":"object","required":["surface","priority","part_of_speech","part_of_speech_detail_1","part_of_speech_detail_2","part_of_speech_detail_3","inflectional_type","inflectional_form","stem","yomi","pronunciation","accent_type","accent_associative_rule"],"title":"UserDictWord","description":"辞書のコンパイルに使われる情報"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WordTypes":{"type":"string","enum":["PROPER_NOUN","COMMON_NOUN","VERB","ADJECTIVE","SUFFIX"],"title":"WordTypes","description":"品詞"},"BaseLibraryInfo":{"description":"音声ライブラリの情報","properties":{"name":{"description":"音声ライブラリの名前","title":"Name","type":"string"},"uuid":{"description":"音声ライブラリのUUID","title":"Uuid","type":"string"},"version":{"description":"音声ライブラリのバージョン","title":"Version","type":"string"},"download_url":{"description":"音声ライブラリのダウンロードURL","title":"Download Url","type":"string"},"bytes":{"description":"音声ライブラリのバイト数","title":"Bytes","type":"integer"},"speakers":{"description":"音声ライブラリに含まれるキャラクターのリスト","items":{"$ref":"#/components/schemas/LibrarySpeaker"},"title":"Speakers","type":"array"}},"required":["name","uuid","version","download_url","bytes","speakers"],"title":"BaseLibraryInfo","type":"object"},"VvlibManifest":{"description":"vvlib(VOICEVOX Library)に関する情報","properties":{"manifest_version":{"description":"マニフェストバージョン","title":"Manifest Version","type":"string"},"name":{"description":"音声ライブラリ名","title":"Name","type":"string"},"version":{"description":"音声ライブラリバージョン","title":"Version","type":"string"},"uuid":{"description":"音声ライブラリのUUID","title":"Uuid","type":"string"},"brand_name":{"description":"エンジンのブランド名","title":"Brand Name","type":"string"},"engine_name":{"description":"エンジン名","title":"Engine Name","type":"string"},"engine_uuid":{"description":"エンジンのUUID","title":"Engine Uuid","type":"string"}},"required":["manifest_version","name","version","uuid","brand_name","engine_name","engine_uuid"],"title":"VvlibManifest","type":"object"}}}} \ No newline at end of file +{"openapi":"3.1.0","info":{"title":"DUMMY Engine","description":"DUMMY の音声合成エンジンです。","version":"latest"},"paths":{"/audio_query":{"post":{"tags":["クエリ作成"],"summary":"音声合成用のクエリを作成する","description":"音声合成用のクエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。","operationId":"audio_query_audio_query_post","parameters":[{"name":"text","in":"query","required":true,"schema":{"type":"string","title":"Text"}},{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/audio_query_from_preset":{"post":{"tags":["クエリ作成"],"summary":"音声合成用のクエリをプリセットを用いて作成する","description":"音声合成用のクエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。","operationId":"audio_query_from_preset_audio_query_from_preset_post","parameters":[{"name":"text","in":"query","required":true,"schema":{"type":"string","title":"Text"}},{"name":"preset_id","in":"query","required":true,"schema":{"type":"integer","title":"Preset Id"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/accent_phrases":{"post":{"tags":["クエリ編集"],"summary":"テキストからアクセント句を得る","description":"テキストからアクセント句を得ます。\nis_kanaが`true`のとき、テキストは次のAquesTalk 風記法で解釈されます。デフォルトは`false`です。\n* 全てのカナはカタカナで記述される\n* アクセント句は`/`または`、`で区切る。`、`で区切った場合に限り無音区間が挿入される。\n* カナの手前に`_`を入れるとそのカナは無声化される\n* アクセント位置を`'`で指定する。全てのアクセント句にはアクセント位置を1つ指定する必要がある。\n* アクセント句末に`?`(全角)を入れることにより疑問文の発音ができる。","operationId":"accent_phrases_accent_phrases_post","parameters":[{"name":"text","in":"query","required":true,"schema":{"type":"string","title":"Text"}},{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"is_kana","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Is Kana"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Response Accent Phrases Accent Phrases Post"}}}},"400":{"description":"読み仮名のパースに失敗","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParseKanaBadRequest"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mora_data":{"post":{"tags":["クエリ編集"],"summary":"アクセント句から音高・音素長を得る","operationId":"mora_data_mora_data_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Accent Phrases"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Response Mora Data Mora Data Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mora_length":{"post":{"tags":["クエリ編集"],"summary":"アクセント句から音素長を得る","operationId":"mora_length_mora_length_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Accent Phrases"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Response Mora Length Mora Length Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mora_pitch":{"post":{"tags":["クエリ編集"],"summary":"アクセント句から音高を得る","operationId":"mora_pitch_mora_pitch_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Accent Phrases"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Response Mora Pitch Mora Pitch Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/synthesis":{"post":{"tags":["音声合成"],"summary":"音声合成する","operationId":"synthesis_synthesis_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"enable_interrogative_upspeak","in":"query","required":false,"schema":{"type":"boolean","description":"疑問系のテキストが与えられたら語尾を自動調整する","default":true,"title":"Enable Interrogative Upspeak"},"description":"疑問系のテキストが与えられたら語尾を自動調整する"},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}}},"responses":{"200":{"description":"Successful Response","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/cancellable_synthesis":{"post":{"tags":["音声合成"],"summary":"音声合成する(キャンセル可能)","operationId":"cancellable_synthesis_cancellable_synthesis_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}}},"responses":{"200":{"description":"Successful Response","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/multi_synthesis":{"post":{"tags":["音声合成"],"summary":"複数まとめて音声合成する","operationId":"multi_synthesis_multi_synthesis_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AudioQuery"},"title":"Queries"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sing_frame_audio_query":{"post":{"tags":["クエリ作成"],"summary":"歌唱音声合成用のクエリを作成する","description":"歌唱音声合成用のクエリの初期値を得ます。ここで得られたクエリはそのまま歌唱音声合成に利用できます。各値の意味は`Schemas`を参照してください。","operationId":"sing_frame_audio_query_sing_frame_audio_query_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Score"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FrameAudioQuery"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sing_frame_f0":{"post":{"tags":["クエリ編集"],"summary":"楽譜・歌唱音声合成用のクエリからフレームごとの基本周波数を得る","operationId":"sing_frame_f0_sing_frame_f0_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_sing_frame_f0_sing_frame_f0_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"number"},"title":"Response Sing Frame F0 Sing Frame F0 Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sing_frame_volume":{"post":{"tags":["クエリ編集"],"summary":"楽譜・歌唱音声合成用のクエリからフレームごとの音量を得る","operationId":"sing_frame_volume_sing_frame_volume_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_sing_frame_volume_sing_frame_volume_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"number"},"title":"Response Sing Frame Volume Sing Frame Volume Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/frame_synthesis":{"post":{"tags":["音声合成"],"summary":"Frame Synthesis","description":"歌唱音声合成を行います。","operationId":"frame_synthesis_frame_synthesis_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FrameAudioQuery"}}}},"responses":{"200":{"description":"Successful Response","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/connect_waves":{"post":{"tags":["その他"],"summary":"base64エンコードされた複数のwavデータを一つに結合する","description":"base64エンコードされたwavデータを一纏めにし、wavファイルで返します。","operationId":"connect_waves_connect_waves_post","requestBody":{"content":{"application/json":{"schema":{"items":{"type":"string"},"type":"array","title":"Waves"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/validate_kana":{"post":{"tags":["その他"],"summary":"テキストがAquesTalk 風記法に従っているか判定する","description":"テキストがAquesTalk 風記法に従っているかどうかを判定します。\n従っていない場合はエラーが返ります。","operationId":"validate_kana_validate_kana_post","parameters":[{"name":"text","in":"query","required":true,"schema":{"type":"string","description":"判定する対象の文字列","title":"Text"},"description":"判定する対象の文字列"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"boolean","title":"Response Validate Kana Validate Kana Post"}}}},"400":{"description":"テキストが不正です","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParseKanaBadRequest"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/initialize_speaker":{"post":{"tags":["その他"],"summary":"Initialize Speaker","description":"指定されたスタイルを初期化します。\n実行しなくても他のAPIは使用できますが、初回実行時に時間がかかることがあります。","operationId":"initialize_speaker_initialize_speaker_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"skip_reinit","in":"query","required":false,"schema":{"type":"boolean","description":"既に初期化済みのスタイルの再初期化をスキップするかどうか","default":false,"title":"Skip Reinit"},"description":"既に初期化済みのスタイルの再初期化をスキップするかどうか"},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/is_initialized_speaker":{"get":{"tags":["その他"],"summary":"Is Initialized Speaker","description":"指定されたスタイルが初期化されているかどうかを返します。","operationId":"is_initialized_speaker_is_initialized_speaker_get","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"boolean","title":"Response Is Initialized Speaker Is Initialized Speaker Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/supported_devices":{"get":{"tags":["その他"],"summary":"Supported Devices","description":"対応デバイスの一覧を取得します。","operationId":"supported_devices_supported_devices_get","parameters":[{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportedDevicesInfo"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/morphable_targets":{"post":{"tags":["音声合成"],"summary":"指定したスタイルに対してエンジン内のキャラクターがモーフィングが可能か判定する","description":"指定されたベーススタイルに対してエンジン内の各キャラクターがモーフィング機能を利用可能か返します。\nモーフィングの許可/禁止は`/speakers`の`speaker.supported_features.synthesis_morphing`に記載されています。\nプロパティが存在しない場合は、モーフィングが許可されているとみなします。\n返り値のスタイルIDはstring型なので注意。","operationId":"morphable_targets_morphable_targets_post","parameters":[{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"type":"integer"},"title":"Base Style Ids"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/MorphableTargetInfo"}},"title":"Response Morphable Targets Morphable Targets Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/synthesis_morphing":{"post":{"tags":["音声合成"],"summary":"2種類のスタイルでモーフィングした音声を合成する","description":"指定された2種類のスタイルで音声を合成、指定した割合でモーフィングした音声を得ます。\nモーフィングの割合は`morph_rate`で指定でき、0.0でベースのスタイル、1.0でターゲットのスタイルに近づきます。","operationId":"_synthesis_morphing_synthesis_morphing_post","parameters":[{"name":"base_speaker","in":"query","required":true,"schema":{"type":"integer","title":"Base Speaker"}},{"name":"target_speaker","in":"query","required":true,"schema":{"type":"integer","title":"Target Speaker"}},{"name":"morph_rate","in":"query","required":true,"schema":{"type":"number","maximum":1.0,"minimum":0.0,"title":"Morph Rate"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}}},"responses":{"200":{"description":"Successful Response","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/presets":{"get":{"tags":["その他"],"summary":"Get Presets","description":"エンジンが保持しているプリセットの設定を返します","operationId":"get_presets_presets_get","responses":{"200":{"description":"プリセットのリスト","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Preset"},"type":"array","title":"Response Get Presets Presets Get"}}}}}}},"/add_preset":{"post":{"tags":["その他"],"summary":"Add Preset","description":"新しいプリセットを追加します","operationId":"add_preset_add_preset_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Preset","description":"新しいプリセット。プリセットIDが既存のものと重複している場合は、新規のプリセットIDが採番されます。"}}},"required":true},"responses":{"200":{"description":"追加したプリセットのプリセットID","content":{"application/json":{"schema":{"type":"integer","title":"Response Add Preset Add Preset Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/update_preset":{"post":{"tags":["その他"],"summary":"Update Preset","description":"既存のプリセットを更新します","operationId":"update_preset_update_preset_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Preset","description":"更新するプリセット。プリセットIDが更新対象と一致している必要があります。"}}},"required":true},"responses":{"200":{"description":"更新したプリセットのプリセットID","content":{"application/json":{"schema":{"type":"integer","title":"Response Update Preset Update Preset Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/delete_preset":{"post":{"tags":["その他"],"summary":"Delete Preset","description":"既存のプリセットを削除します","operationId":"delete_preset_delete_preset_post","parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"integer","description":"削除するプリセットのプリセットID","title":"Id"},"description":"削除するプリセットのプリセットID"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/speakers":{"get":{"tags":["その他"],"summary":"Speakers","description":"喋れるキャラクターの情報の一覧を返します。","operationId":"speakers_speakers_get","parameters":[{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Speaker"},"title":"Response Speakers Speakers Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/speaker_info":{"get":{"tags":["その他"],"summary":"Speaker Info","description":"UUID で指定された喋れるキャラクターの情報を返します。\n画像や音声はresource_formatで指定した形式で返されます。","operationId":"speaker_info_speaker_info_get","parameters":[{"name":"speaker_uuid","in":"query","required":true,"schema":{"type":"string","title":"Speaker Uuid"}},{"name":"resource_format","in":"query","required":false,"schema":{"enum":["base64","url"],"type":"string","default":"base64","title":"Resource Format"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpeakerInfo"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/singers":{"get":{"tags":["その他"],"summary":"Singers","description":"歌えるキャラクターの情報の一覧を返します。","operationId":"singers_singers_get","parameters":[{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Speaker"},"title":"Response Singers Singers Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/singer_info":{"get":{"tags":["その他"],"summary":"Singer Info","description":"UUID で指定された歌えるキャラクターの情報を返します。\n画像や音声はresource_formatで指定した形式で返されます。","operationId":"singer_info_singer_info_get","parameters":[{"name":"speaker_uuid","in":"query","required":true,"schema":{"type":"string","title":"Speaker Uuid"}},{"name":"resource_format","in":"query","required":false,"schema":{"enum":["base64","url"],"type":"string","default":"base64","title":"Resource Format"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpeakerInfo"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/downloadable_libraries":{"get":{"tags":["音声ライブラリ管理"],"summary":"Downloadable Libraries","description":"ダウンロード可能な音声ライブラリの情報を返します。","operationId":"downloadable_libraries_downloadable_libraries_get","responses":{"200":{"description":"ダウンロード可能な音声ライブラリの情報リスト","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/DownloadableLibraryInfo"},"type":"array","title":"Response Downloadable Libraries Downloadable Libraries Get"}}}}}}},"/installed_libraries":{"get":{"tags":["音声ライブラリ管理"],"summary":"Installed Libraries","description":"インストールした音声ライブラリの情報を返します。","operationId":"installed_libraries_installed_libraries_get","responses":{"200":{"description":"インストールした音声ライブラリの情報","content":{"application/json":{"schema":{"additionalProperties":{"$ref":"#/components/schemas/InstalledLibraryInfo"},"type":"object","title":"Response Installed Libraries Installed Libraries Get"}}}}}}},"/install_library/{library_uuid}":{"post":{"tags":["音声ライブラリ管理"],"summary":"Install Library","description":"音声ライブラリをインストールします。\n音声ライブラリのZIPファイルをリクエストボディとして送信してください。","operationId":"install_library_install_library__library_uuid__post","parameters":[{"name":"library_uuid","in":"path","required":true,"schema":{"type":"string","description":"音声ライブラリのID","title":"Library Uuid"},"description":"音声ライブラリのID"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/uninstall_library/{library_uuid}":{"post":{"tags":["音声ライブラリ管理"],"summary":"Uninstall Library","description":"音声ライブラリをアンインストールします。","operationId":"uninstall_library_uninstall_library__library_uuid__post","parameters":[{"name":"library_uuid","in":"path","required":true,"schema":{"type":"string","description":"音声ライブラリのID","title":"Library Uuid"},"description":"音声ライブラリのID"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user_dict":{"get":{"tags":["ユーザー辞書"],"summary":"Get User Dict Words","description":"ユーザー辞書に登録されている単語の一覧を返します。\n単語の表層形(surface)は正規化済みの物を返します。","operationId":"get_user_dict_words_user_dict_get","responses":{"200":{"description":"単語のUUIDとその詳細","content":{"application/json":{"schema":{"additionalProperties":{"$ref":"#/components/schemas/UserDictWord"},"type":"object","title":"Response Get User Dict Words User Dict Get"}}}}}}},"/user_dict_word":{"post":{"tags":["ユーザー辞書"],"summary":"Add User Dict Word","description":"ユーザー辞書に言葉を追加します。","operationId":"add_user_dict_word_user_dict_word_post","parameters":[{"name":"surface","in":"query","required":true,"schema":{"type":"string","description":"言葉の表層形","title":"Surface"},"description":"言葉の表層形"},{"name":"pronunciation","in":"query","required":true,"schema":{"type":"string","description":"言葉の発音(カタカナ)","title":"Pronunciation"},"description":"言葉の発音(カタカナ)"},{"name":"accent_type","in":"query","required":true,"schema":{"type":"integer","description":"アクセント型(音が下がる場所を指す)","title":"Accent Type"},"description":"アクセント型(音が下がる場所を指す)"},{"name":"word_type","in":"query","required":false,"schema":{"$ref":"#/components/schemas/WordTypes","description":"PROPER_NOUN(固有名詞)、COMMON_NOUN(普通名詞)、VERB(動詞)、ADJECTIVE(形容詞)、SUFFIX(語尾)のいずれか"},"description":"PROPER_NOUN(固有名詞)、COMMON_NOUN(普通名詞)、VERB(動詞)、ADJECTIVE(形容詞)、SUFFIX(語尾)のいずれか"},{"name":"priority","in":"query","required":false,"schema":{"type":"integer","description":"単語の優先度(0から10までの整数)。数字が大きいほど優先度が高くなる。1から9までの値を指定することを推奨","maximum":10,"minimum":0,"title":"Priority"},"description":"単語の優先度(0から10までの整数)。数字が大きいほど優先度が高くなる。1から9までの値を指定することを推奨"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"string","title":"Response Add User Dict Word User Dict Word Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user_dict_word/{word_uuid}":{"put":{"tags":["ユーザー辞書"],"summary":"Rewrite User Dict Word","description":"ユーザー辞書に登録されている言葉を更新します。","operationId":"rewrite_user_dict_word_user_dict_word__word_uuid__put","parameters":[{"name":"word_uuid","in":"path","required":true,"schema":{"type":"string","description":"更新する言葉のUUID","title":"Word Uuid"},"description":"更新する言葉のUUID"},{"name":"surface","in":"query","required":true,"schema":{"type":"string","description":"言葉の表層形","title":"Surface"},"description":"言葉の表層形"},{"name":"pronunciation","in":"query","required":true,"schema":{"type":"string","description":"言葉の発音(カタカナ)","title":"Pronunciation"},"description":"言葉の発音(カタカナ)"},{"name":"accent_type","in":"query","required":true,"schema":{"type":"integer","description":"アクセント型(音が下がる場所を指す)","title":"Accent Type"},"description":"アクセント型(音が下がる場所を指す)"},{"name":"word_type","in":"query","required":false,"schema":{"$ref":"#/components/schemas/WordTypes","description":"PROPER_NOUN(固有名詞)、COMMON_NOUN(普通名詞)、VERB(動詞)、ADJECTIVE(形容詞)、SUFFIX(語尾)のいずれか"},"description":"PROPER_NOUN(固有名詞)、COMMON_NOUN(普通名詞)、VERB(動詞)、ADJECTIVE(形容詞)、SUFFIX(語尾)のいずれか"},{"name":"priority","in":"query","required":false,"schema":{"type":"integer","description":"単語の優先度(0から10までの整数)。数字が大きいほど優先度が高くなる。1から9までの値を指定することを推奨。","maximum":10,"minimum":0,"title":"Priority"},"description":"単語の優先度(0から10までの整数)。数字が大きいほど優先度が高くなる。1から9までの値を指定することを推奨。"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["ユーザー辞書"],"summary":"Delete User Dict Word","description":"ユーザー辞書に登録されている言葉を削除します。","operationId":"delete_user_dict_word_user_dict_word__word_uuid__delete","parameters":[{"name":"word_uuid","in":"path","required":true,"schema":{"type":"string","description":"削除する言葉のUUID","title":"Word Uuid"},"description":"削除する言葉のUUID"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/import_user_dict":{"post":{"tags":["ユーザー辞書"],"summary":"Import User Dict Words","description":"他のユーザー辞書をインポートします。","operationId":"import_user_dict_words_import_user_dict_post","parameters":[{"name":"override","in":"query","required":true,"schema":{"type":"boolean","description":"重複したエントリがあった場合、上書きするかどうか","title":"Override"},"description":"重複したエントリがあった場合、上書きするかどうか"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/UserDictWord"},"description":"インポートするユーザー辞書のデータ","title":"Import Dict Data"}}}},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/version":{"get":{"tags":["その他"],"summary":"Version","description":"エンジンのバージョンを取得します。","operationId":"version_version_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"string","title":"Response Version Version Get"}}}}}}},"/core_versions":{"get":{"tags":["その他"],"summary":"Core Versions","description":"利用可能なコアのバージョン一覧を取得します。","operationId":"core_versions_core_versions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"type":"string"},"type":"array","title":"Response Core Versions Core Versions Get"}}}}}}},"/engine_manifest":{"get":{"tags":["その他"],"summary":"Engine Manifest","description":"エンジンマニフェストを取得します。","operationId":"engine_manifest_engine_manifest_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EngineManifest"}}}}}}},"/setting":{"get":{"tags":["設定"],"summary":"Setting Get","description":"設定ページを返します。","operationId":"setting_get_setting_get","responses":{"200":{"description":"Successful Response"}}},"post":{"tags":["設定"],"summary":"Setting Post","description":"設定を更新します。","operationId":"setting_post_setting_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_setting_post_setting_post"}}},"required":true},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"tags":["その他"],"summary":"Get Portal Page","description":"ポータルページを返します。","operationId":"get_portal_page__get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}}},"components":{"schemas":{"AccentPhrase":{"properties":{"moras":{"items":{"$ref":"#/components/schemas/Mora"},"type":"array","title":"Moras","description":"モーラのリスト"},"accent":{"type":"integer","title":"Accent","description":"アクセント箇所"},"pause_mora":{"$ref":"#/components/schemas/Mora","title":"Pause Mora","description":"後ろに無音を付けるかどうか"},"is_interrogative":{"type":"boolean","title":"Is Interrogative","description":"疑問系かどうか","default":false}},"type":"object","required":["moras","accent"],"title":"AccentPhrase","description":"アクセント句ごとの情報"},"AudioQuery":{"properties":{"accent_phrases":{"items":{"$ref":"#/components/schemas/AccentPhrase"},"type":"array","title":"Accent Phrases","description":"アクセント句のリスト"},"speedScale":{"type":"number","title":"Speedscale","description":"全体の話速"},"pitchScale":{"type":"number","title":"Pitchscale","description":"全体の音高"},"intonationScale":{"type":"number","title":"Intonationscale","description":"全体の抑揚"},"volumeScale":{"type":"number","title":"Volumescale","description":"全体の音量"},"prePhonemeLength":{"type":"number","title":"Prephonemelength","description":"音声の前の無音時間"},"postPhonemeLength":{"type":"number","title":"Postphonemelength","description":"音声の後の無音時間"},"pauseLength":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Pauselength","description":"句読点などの無音時間。nullのときは無視される。デフォルト値はnull"},"pauseLengthScale":{"type":"number","title":"Pauselengthscale","description":"句読点などの無音時間(倍率)。デフォルト値は1","default":1},"outputSamplingRate":{"type":"integer","title":"Outputsamplingrate","description":"音声データの出力サンプリングレート"},"outputStereo":{"type":"boolean","title":"Outputstereo","description":"音声データをステレオ出力するか否か"},"kana":{"type":"string","title":"Kana","description":"[読み取り専用]AquesTalk 風記法によるテキスト。音声合成用のクエリとしては無視される"}},"type":"object","required":["accent_phrases","speedScale","pitchScale","intonationScale","volumeScale","prePhonemeLength","postPhonemeLength","outputSamplingRate","outputStereo"],"title":"AudioQuery","description":"音声合成用のクエリ"},"Body_setting_post_setting_post":{"properties":{"cors_policy_mode":{"$ref":"#/components/schemas/CorsPolicyMode"},"allow_origin":{"type":"string","title":"Allow Origin"}},"type":"object","required":["cors_policy_mode"],"title":"Body_setting_post_setting_post"},"Body_sing_frame_f0_sing_frame_f0_post":{"properties":{"score":{"$ref":"#/components/schemas/Score"},"frame_audio_query":{"$ref":"#/components/schemas/FrameAudioQuery"}},"type":"object","required":["score","frame_audio_query"],"title":"Body_sing_frame_f0_sing_frame_f0_post"},"Body_sing_frame_volume_sing_frame_volume_post":{"properties":{"score":{"$ref":"#/components/schemas/Score"},"frame_audio_query":{"$ref":"#/components/schemas/FrameAudioQuery"}},"type":"object","required":["score","frame_audio_query"],"title":"Body_sing_frame_volume_sing_frame_volume_post"},"CorsPolicyMode":{"type":"string","enum":["all","localapps"],"title":"CorsPolicyMode","description":"CORSの許可モード"},"DownloadableLibraryInfo":{"properties":{"name":{"type":"string","title":"Name","description":"音声ライブラリの名前"},"uuid":{"type":"string","title":"Uuid","description":"音声ライブラリのUUID"},"version":{"type":"string","title":"Version","description":"音声ライブラリのバージョン"},"download_url":{"type":"string","title":"Download Url","description":"音声ライブラリのダウンロードURL"},"bytes":{"type":"integer","title":"Bytes","description":"音声ライブラリのバイト数"},"speakers":{"items":{"$ref":"#/components/schemas/LibrarySpeaker"},"type":"array","title":"Speakers","description":"音声ライブラリに含まれるキャラクターのリスト"}},"type":"object","required":["name","uuid","version","download_url","bytes","speakers"],"title":"DownloadableLibraryInfo","description":"ダウンロード可能な音声ライブラリの情報"},"EngineManifest":{"properties":{"manifest_version":{"type":"string","title":"Manifest Version","description":"マニフェストのバージョン"},"name":{"type":"string","title":"Name","description":"エンジン名"},"brand_name":{"type":"string","title":"Brand Name","description":"ブランド名"},"uuid":{"type":"string","title":"Uuid","description":"エンジンのUUID"},"url":{"type":"string","title":"Url","description":"エンジンのURL"},"icon":{"type":"string","title":"Icon","description":"エンジンのアイコンをBASE64エンコードしたもの"},"default_sampling_rate":{"type":"integer","title":"Default Sampling Rate","description":"デフォルトのサンプリング周波数"},"frame_rate":{"type":"number","title":"Frame Rate","description":"エンジンのフレームレート"},"terms_of_service":{"type":"string","title":"Terms Of Service","description":"エンジンの利用規約"},"update_infos":{"items":{"$ref":"#/components/schemas/UpdateInfo"},"type":"array","title":"Update Infos","description":"エンジンのアップデート情報"},"dependency_licenses":{"items":{"$ref":"#/components/schemas/LicenseInfo"},"type":"array","title":"Dependency Licenses","description":"依存関係のライセンス情報"},"supported_vvlib_manifest_version":{"type":"string","title":"Supported Vvlib Manifest Version","description":"エンジンが対応するvvlibのバージョン"},"supported_features":{"$ref":"#/components/schemas/SupportedFeatures","description":"エンジンが持つ機能"}},"type":"object","required":["manifest_version","name","brand_name","uuid","url","icon","default_sampling_rate","frame_rate","terms_of_service","update_infos","dependency_licenses","supported_features"],"title":"EngineManifest","description":"エンジン自体に関する情報"},"FrameAudioQuery":{"properties":{"f0":{"items":{"type":"number"},"type":"array","title":"F0","description":"フレームごとの基本周波数"},"volume":{"items":{"type":"number"},"type":"array","title":"Volume","description":"フレームごとの音量"},"phonemes":{"items":{"$ref":"#/components/schemas/FramePhoneme"},"type":"array","title":"Phonemes","description":"音素のリスト"},"volumeScale":{"type":"number","title":"Volumescale","description":"全体の音量"},"outputSamplingRate":{"type":"integer","title":"Outputsamplingrate","description":"音声データの出力サンプリングレート"},"outputStereo":{"type":"boolean","title":"Outputstereo","description":"音声データをステレオ出力するか否か"}},"type":"object","required":["f0","volume","phonemes","volumeScale","outputSamplingRate","outputStereo"],"title":"FrameAudioQuery","description":"フレームごとの音声合成用のクエリ"},"FramePhoneme":{"properties":{"phoneme":{"type":"string","title":"Phoneme","description":"音素"},"frame_length":{"type":"integer","title":"Frame Length","description":"音素のフレーム長"},"note_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note Id","description":"音符のID"}},"type":"object","required":["phoneme","frame_length"],"title":"FramePhoneme","description":"音素の情報"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"InstalledLibraryInfo":{"properties":{"name":{"type":"string","title":"Name","description":"音声ライブラリの名前"},"uuid":{"type":"string","title":"Uuid","description":"音声ライブラリのUUID"},"version":{"type":"string","title":"Version","description":"音声ライブラリのバージョン"},"download_url":{"type":"string","title":"Download Url","description":"音声ライブラリのダウンロードURL"},"bytes":{"type":"integer","title":"Bytes","description":"音声ライブラリのバイト数"},"speakers":{"items":{"$ref":"#/components/schemas/LibrarySpeaker"},"type":"array","title":"Speakers","description":"音声ライブラリに含まれるキャラクターのリスト"},"uninstallable":{"type":"boolean","title":"Uninstallable","description":"アンインストール可能かどうか"}},"type":"object","required":["name","uuid","version","download_url","bytes","speakers","uninstallable"],"title":"InstalledLibraryInfo","description":"インストール済み音声ライブラリの情報"},"LibrarySpeaker":{"properties":{"speaker":{"$ref":"#/components/schemas/Speaker"},"speaker_info":{"$ref":"#/components/schemas/SpeakerInfo"}},"type":"object","required":["speaker","speaker_info"],"title":"LibrarySpeaker","description":"音声ライブラリに含まれるキャラクターの情報"},"LicenseInfo":{"properties":{"name":{"type":"string","title":"Name","description":"依存ライブラリ名"},"version":{"type":"string","title":"Version","description":"依存ライブラリのバージョン"},"license":{"type":"string","title":"License","description":"依存ライブラリのライセンス名"},"text":{"type":"string","title":"Text","description":"依存ライブラリのライセンス本文"}},"type":"object","required":["name","text"],"title":"LicenseInfo","description":"依存ライブラリのライセンス情報"},"Mora":{"properties":{"text":{"type":"string","title":"Text","description":"文字"},"consonant":{"type":"string","title":"Consonant","description":"子音の音素"},"consonant_length":{"type":"number","title":"Consonant Length","description":"子音の音長"},"vowel":{"type":"string","title":"Vowel","description":"母音の音素"},"vowel_length":{"type":"number","title":"Vowel Length","description":"母音の音長"},"pitch":{"type":"number","title":"Pitch","description":"音高"}},"type":"object","required":["text","vowel","vowel_length","pitch"],"title":"Mora","description":"モーラ(子音+母音)ごとの情報"},"MorphableTargetInfo":{"properties":{"is_morphable":{"type":"boolean","title":"Is Morphable","description":"指定したキャラクターに対してモーフィングの可否"}},"type":"object","required":["is_morphable"],"title":"MorphableTargetInfo"},"Note":{"properties":{"id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Id","description":"ID"},"key":{"type":"integer","title":"Key","description":"音階"},"frame_length":{"type":"integer","title":"Frame Length","description":"音符のフレーム長"},"lyric":{"type":"string","title":"Lyric","description":"音符の歌詞"}},"type":"object","required":["frame_length","lyric"],"title":"Note","description":"音符ごとの情報"},"ParseKanaBadRequest":{"properties":{"text":{"type":"string","title":"Text","description":"エラーメッセージ"},"error_name":{"type":"string","title":"Error Name","description":"エラー名\n\n|name|description|\n|---|---|\n| UNKNOWN_TEXT | 判別できない読み仮名があります: {text} |\n| ACCENT_TOP | 句頭にアクセントは置けません: {text} |\n| ACCENT_TWICE | 1つのアクセント句に二つ以上のアクセントは置けません: {text} |\n| ACCENT_NOTFOUND | アクセントを指定していないアクセント句があります: {text} |\n| EMPTY_PHRASE | {position}番目のアクセント句が空白です |\n| INTERROGATION_MARK_NOT_AT_END | アクセント句末以外に「?」は置けません: {text} |\n| INFINITE_LOOP | 処理時に無限ループになってしまいました...バグ報告をお願いします。 |"},"error_args":{"additionalProperties":{"type":"string"},"type":"object","title":"Error Args","description":"エラーを起こした箇所"}},"type":"object","required":["text","error_name","error_args"],"title":"ParseKanaBadRequest"},"Preset":{"properties":{"id":{"type":"integer","title":"Id","description":"プリセットID"},"name":{"type":"string","title":"Name","description":"プリセット名"},"speaker_uuid":{"type":"string","title":"Speaker Uuid","description":"キャラクターのUUID"},"style_id":{"type":"integer","title":"Style Id","description":"スタイルID"},"speedScale":{"type":"number","title":"Speedscale","description":"全体の話速"},"pitchScale":{"type":"number","title":"Pitchscale","description":"全体の音高"},"intonationScale":{"type":"number","title":"Intonationscale","description":"全体の抑揚"},"volumeScale":{"type":"number","title":"Volumescale","description":"全体の音量"},"prePhonemeLength":{"type":"number","title":"Prephonemelength","description":"音声の前の無音時間"},"postPhonemeLength":{"type":"number","title":"Postphonemelength","description":"音声の後の無音時間"},"pauseLength":{"type":"number","title":"Pauselength","description":"句読点などの無音時間"},"pauseLengthScale":{"type":"number","title":"Pauselengthscale","description":"句読点などの無音時間(倍率)","default":1}},"type":"object","required":["id","name","speaker_uuid","style_id","speedScale","pitchScale","intonationScale","volumeScale","prePhonemeLength","postPhonemeLength"],"title":"Preset","description":"プリセット情報"},"Score":{"properties":{"notes":{"items":{"$ref":"#/components/schemas/Note"},"type":"array","title":"Notes","description":"音符のリスト"}},"type":"object","required":["notes"],"title":"Score","description":"楽譜情報"},"Speaker":{"properties":{"name":{"type":"string","title":"Name","description":"名前"},"speaker_uuid":{"type":"string","title":"Speaker Uuid","description":"キャラクターのUUID"},"styles":{"items":{"$ref":"#/components/schemas/SpeakerStyle"},"type":"array","title":"Styles","description":"スタイルの一覧"},"version":{"type":"string","title":"Version","description":"キャラクターのバージョン"},"supported_features":{"$ref":"#/components/schemas/SpeakerSupportedFeatures","description":"キャラクターの対応機能"}},"type":"object","required":["name","speaker_uuid","styles","version"],"title":"Speaker","description":"キャラクター情報"},"SpeakerInfo":{"properties":{"policy":{"type":"string","title":"Policy","description":"policy.md"},"portrait":{"type":"string","title":"Portrait","description":"立ち絵画像をbase64エンコードしたもの、あるいはURL"},"style_infos":{"items":{"$ref":"#/components/schemas/StyleInfo"},"type":"array","title":"Style Infos","description":"スタイルの追加情報"}},"type":"object","required":["policy","portrait","style_infos"],"title":"SpeakerInfo","description":"キャラクターの追加情報"},"SpeakerStyle":{"properties":{"name":{"type":"string","title":"Name","description":"スタイル名"},"id":{"type":"integer","title":"Id","description":"スタイルID"},"type":{"type":"string","enum":["talk","singing_teacher","frame_decode","sing"],"title":"Type","description":"スタイルの種類。talk:音声合成クエリの作成と音声合成が可能。singing_teacher:歌唱音声合成用のクエリの作成が可能。frame_decode:歌唱音声合成が可能。sing:歌唱音声合成用のクエリの作成と歌唱音声合成が可能。","default":"talk"}},"type":"object","required":["name","id"],"title":"SpeakerStyle","description":"キャラクターのスタイル情報"},"SpeakerSupportedFeatures":{"properties":{"permitted_synthesis_morphing":{"type":"string","enum":["ALL","SELF_ONLY","NOTHING"],"title":"Permitted Synthesis Morphing","description":"モーフィング機能への対応。'ALL' は「全て許可」、'SELF_ONLY' は「同じキャラクター内でのみ許可」、'NOTHING' は「全て禁止」","default":"ALL"}},"type":"object","title":"SpeakerSupportedFeatures","description":"キャラクターの対応機能の情報"},"StyleInfo":{"properties":{"id":{"type":"integer","title":"Id","description":"スタイルID"},"icon":{"type":"string","title":"Icon","description":"このスタイルのアイコンをbase64エンコードしたもの、あるいはURL"},"portrait":{"type":"string","title":"Portrait","description":"このスタイルの立ち絵画像をbase64エンコードしたもの、あるいはURL"},"voice_samples":{"items":{"type":"string"},"type":"array","title":"Voice Samples","description":"サンプル音声をbase64エンコードしたもの、あるいはURL"}},"type":"object","required":["id","icon","voice_samples"],"title":"StyleInfo","description":"スタイルの追加情報"},"SupportedDevicesInfo":{"properties":{"cpu":{"type":"boolean","title":"Cpu","description":"CPUに対応しているか"},"cuda":{"type":"boolean","title":"Cuda","description":"CUDA(Nvidia GPU)に対応しているか"},"dml":{"type":"boolean","title":"Dml","description":"DirectML(Nvidia GPU/Radeon GPU等)に対応しているか"}},"type":"object","required":["cpu","cuda","dml"],"title":"SupportedDevicesInfo","description":"対応しているデバイスの情報"},"SupportedFeatures":{"properties":{"adjust_mora_pitch":{"type":"boolean","title":"Adjust Mora Pitch","description":"モーラごとの音高の調整"},"adjust_phoneme_length":{"type":"boolean","title":"Adjust Phoneme Length","description":"音素ごとの長さの調整"},"adjust_speed_scale":{"type":"boolean","title":"Adjust Speed Scale","description":"全体の話速の調整"},"adjust_pitch_scale":{"type":"boolean","title":"Adjust Pitch Scale","description":"全体の音高の調整"},"adjust_intonation_scale":{"type":"boolean","title":"Adjust Intonation Scale","description":"全体の抑揚の調整"},"adjust_volume_scale":{"type":"boolean","title":"Adjust Volume Scale","description":"全体の音量の調整"},"adjust_pause_length":{"type":"boolean","title":"Adjust Pause Length","description":"句読点などの無音時間の調整"},"interrogative_upspeak":{"type":"boolean","title":"Interrogative Upspeak","description":"疑問文の自動調整"},"synthesis_morphing":{"type":"boolean","title":"Synthesis Morphing","description":"2種類のスタイルでモーフィングした音声を合成"},"sing":{"type":"boolean","title":"Sing","description":"歌唱音声合成"},"manage_library":{"type":"boolean","title":"Manage Library","description":"音声ライブラリのインストール・アンインストール"},"return_resource_url":{"type":"boolean","title":"Return Resource Url","description":"キャラクター情報のリソースをURLで返送"}},"type":"object","required":["adjust_mora_pitch","adjust_phoneme_length","adjust_speed_scale","adjust_pitch_scale","adjust_intonation_scale","adjust_volume_scale","interrogative_upspeak","synthesis_morphing"],"title":"SupportedFeatures","description":"エンジンが持つ機能の一覧"},"UpdateInfo":{"properties":{"version":{"type":"string","title":"Version","description":"エンジンのバージョン名"},"descriptions":{"items":{"type":"string"},"type":"array","title":"Descriptions","description":"アップデートの詳細についての説明"},"contributors":{"items":{"type":"string"},"type":"array","title":"Contributors","description":"貢献者名"}},"type":"object","required":["version","descriptions"],"title":"UpdateInfo","description":"エンジンのアップデート情報"},"UserDictWord":{"properties":{"surface":{"type":"string","title":"Surface","description":"表層形"},"priority":{"type":"integer","maximum":10.0,"minimum":0.0,"title":"Priority","description":"優先度"},"context_id":{"type":"integer","title":"Context Id","description":"文脈ID","default":1348},"part_of_speech":{"type":"string","title":"Part Of Speech","description":"品詞"},"part_of_speech_detail_1":{"type":"string","title":"Part Of Speech Detail 1","description":"品詞細分類1"},"part_of_speech_detail_2":{"type":"string","title":"Part Of Speech Detail 2","description":"品詞細分類2"},"part_of_speech_detail_3":{"type":"string","title":"Part Of Speech Detail 3","description":"品詞細分類3"},"inflectional_type":{"type":"string","title":"Inflectional Type","description":"活用型"},"inflectional_form":{"type":"string","title":"Inflectional Form","description":"活用形"},"stem":{"type":"string","title":"Stem","description":"原形"},"yomi":{"type":"string","title":"Yomi","description":"読み"},"pronunciation":{"type":"string","title":"Pronunciation","description":"発音"},"accent_type":{"type":"integer","title":"Accent Type","description":"アクセント型"},"mora_count":{"type":"integer","title":"Mora Count","description":"モーラ数"},"accent_associative_rule":{"type":"string","title":"Accent Associative Rule","description":"アクセント結合規則"}},"type":"object","required":["surface","priority","part_of_speech","part_of_speech_detail_1","part_of_speech_detail_2","part_of_speech_detail_3","inflectional_type","inflectional_form","stem","yomi","pronunciation","accent_type","accent_associative_rule"],"title":"UserDictWord","description":"辞書のコンパイルに使われる情報"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WordTypes":{"type":"string","enum":["PROPER_NOUN","COMMON_NOUN","VERB","ADJECTIVE","SUFFIX"],"title":"WordTypes","description":"品詞"},"BaseLibraryInfo":{"description":"音声ライブラリの情報","properties":{"name":{"description":"音声ライブラリの名前","title":"Name","type":"string"},"uuid":{"description":"音声ライブラリのUUID","title":"Uuid","type":"string"},"version":{"description":"音声ライブラリのバージョン","title":"Version","type":"string"},"download_url":{"description":"音声ライブラリのダウンロードURL","title":"Download Url","type":"string"},"bytes":{"description":"音声ライブラリのバイト数","title":"Bytes","type":"integer"},"speakers":{"description":"音声ライブラリに含まれるキャラクターのリスト","items":{"$ref":"#/components/schemas/LibrarySpeaker"},"title":"Speakers","type":"array"}},"required":["name","uuid","version","download_url","bytes","speakers"],"title":"BaseLibraryInfo","type":"object"},"VvlibManifest":{"description":"vvlib(VOICEVOX Library)に関する情報","properties":{"manifest_version":{"description":"マニフェストバージョン","title":"Manifest Version","type":"string"},"name":{"description":"音声ライブラリ名","title":"Name","type":"string"},"version":{"description":"音声ライブラリバージョン","title":"Version","type":"string"},"uuid":{"description":"音声ライブラリのUUID","title":"Uuid","type":"string"},"brand_name":{"description":"エンジンのブランド名","title":"Brand Name","type":"string"},"engine_name":{"description":"エンジン名","title":"Engine Name","type":"string"},"engine_uuid":{"description":"エンジンのUUID","title":"Engine Uuid","type":"string"}},"required":["manifest_version","name","version","uuid","brand_name","engine_name","engine_uuid"],"title":"VvlibManifest","type":"object"}}}} \ No newline at end of file diff --git a/src/openapi/.openapi-generator/FILES b/src/openapi/.openapi-generator/FILES index 06624bd77c..44234c1771 100644 --- a/src/openapi/.openapi-generator/FILES +++ b/src/openapi/.openapi-generator/FILES @@ -4,6 +4,7 @@ index.ts models/AccentPhrase.ts models/AudioQuery.ts models/BaseLibraryInfo.ts +models/BodySingFrameF0SingFrameF0Post.ts models/BodySingFrameVolumeSingFrameVolumePost.ts models/CorsPolicyMode.ts models/DownloadableLibraryInfo.ts diff --git a/src/openapi/apis/DefaultApi.ts b/src/openapi/apis/DefaultApi.ts index fbbdd86629..8718b36686 100644 --- a/src/openapi/apis/DefaultApi.ts +++ b/src/openapi/apis/DefaultApi.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * @@ -17,6 +17,7 @@ import * as runtime from '../runtime'; import type { AccentPhrase, AudioQuery, + BodySingFrameF0SingFrameF0Post, BodySingFrameVolumeSingFrameVolumePost, CorsPolicyMode, DownloadableLibraryInfo, @@ -39,6 +40,8 @@ import { AccentPhraseToJSON, AudioQueryFromJSON, AudioQueryToJSON, + BodySingFrameF0SingFrameF0PostFromJSON, + BodySingFrameF0SingFrameF0PostToJSON, BodySingFrameVolumeSingFrameVolumePostFromJSON, BodySingFrameVolumeSingFrameVolumePostToJSON, CorsPolicyModeFromJSON, @@ -197,6 +200,12 @@ export interface SingFrameAudioQuerySingFrameAudioQueryPostRequest { coreVersion?: string; } +export interface SingFrameF0SingFrameF0PostRequest { + speaker: number; + bodySingFrameF0SingFrameF0Post: BodySingFrameF0SingFrameF0Post; + coreVersion?: string; +} + export interface SingFrameVolumeSingFrameVolumePostRequest { speaker: number; bodySingFrameVolumeSingFrameVolumePost: BodySingFrameVolumeSingFrameVolumePost; @@ -766,7 +775,24 @@ export interface DefaultApiInterface { /** * - * @summary スコア・歌唱音声合成用のクエリからフレームごとの音量を得る + * @summary 楽譜・歌唱音声合成用のクエリからフレームごとの基本周波数を得る + * @param {number} speaker + * @param {BodySingFrameF0SingFrameF0Post} bodySingFrameF0SingFrameF0Post + * @param {string} [coreVersion] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApiInterface + */ + singFrameF0SingFrameF0PostRaw(requestParameters: SingFrameF0SingFrameF0PostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>>; + + /** + * 楽譜・歌唱音声合成用のクエリからフレームごとの基本周波数を得る + */ + singFrameF0SingFrameF0Post(requestParameters: SingFrameF0SingFrameF0PostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * + * @summary 楽譜・歌唱音声合成用のクエリからフレームごとの音量を得る * @param {number} speaker * @param {BodySingFrameVolumeSingFrameVolumePost} bodySingFrameVolumeSingFrameVolumePost * @param {string} [coreVersion] @@ -777,7 +803,7 @@ export interface DefaultApiInterface { singFrameVolumeSingFrameVolumePostRaw(requestParameters: SingFrameVolumeSingFrameVolumePostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>>; /** - * スコア・歌唱音声合成用のクエリからフレームごとの音量を得る + * 楽譜・歌唱音声合成用のクエリからフレームごとの音量を得る */ singFrameVolumeSingFrameVolumePost(requestParameters: SingFrameVolumeSingFrameVolumePostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; @@ -2188,7 +2214,52 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { } /** - * スコア・歌唱音声合成用のクエリからフレームごとの音量を得る + * 楽譜・歌唱音声合成用のクエリからフレームごとの基本周波数を得る + */ + async singFrameF0SingFrameF0PostRaw(requestParameters: SingFrameF0SingFrameF0PostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>> { + if (requestParameters.speaker === null || requestParameters.speaker === undefined) { + throw new runtime.RequiredError('speaker','Required parameter requestParameters.speaker was null or undefined when calling singFrameF0SingFrameF0Post.'); + } + + if (requestParameters.bodySingFrameF0SingFrameF0Post === null || requestParameters.bodySingFrameF0SingFrameF0Post === undefined) { + throw new runtime.RequiredError('bodySingFrameF0SingFrameF0Post','Required parameter requestParameters.bodySingFrameF0SingFrameF0Post was null or undefined when calling singFrameF0SingFrameF0Post.'); + } + + const queryParameters: any = {}; + + if (requestParameters.speaker !== undefined) { + queryParameters['speaker'] = requestParameters.speaker; + } + + if (requestParameters.coreVersion !== undefined) { + queryParameters['core_version'] = requestParameters.coreVersion; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + const response = await this.request({ + path: `/sing_frame_f0`, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: BodySingFrameF0SingFrameF0PostToJSON(requestParameters.bodySingFrameF0SingFrameF0Post), + }, initOverrides); + + return new runtime.JSONApiResponse(response); + } + + /** + * 楽譜・歌唱音声合成用のクエリからフレームごとの基本周波数を得る + */ + async singFrameF0SingFrameF0Post(requestParameters: SingFrameF0SingFrameF0PostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const response = await this.singFrameF0SingFrameF0PostRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * 楽譜・歌唱音声合成用のクエリからフレームごとの音量を得る */ async singFrameVolumeSingFrameVolumePostRaw(requestParameters: SingFrameVolumeSingFrameVolumePostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>> { if (requestParameters.speaker === null || requestParameters.speaker === undefined) { @@ -2225,7 +2296,7 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { } /** - * スコア・歌唱音声合成用のクエリからフレームごとの音量を得る + * 楽譜・歌唱音声合成用のクエリからフレームごとの音量を得る */ async singFrameVolumeSingFrameVolumePost(requestParameters: SingFrameVolumeSingFrameVolumePostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { const response = await this.singFrameVolumeSingFrameVolumePostRaw(requestParameters, initOverrides); diff --git a/src/openapi/models/AccentPhrase.ts b/src/openapi/models/AccentPhrase.ts index 2df96050d3..88c7fc2b95 100644 --- a/src/openapi/models/AccentPhrase.ts +++ b/src/openapi/models/AccentPhrase.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * @@ -39,7 +39,7 @@ export interface AccentPhrase { */ accent: number; /** - * + * 後ろに無音を付けるかどうか * @type {Mora} * @memberof AccentPhrase */ diff --git a/src/openapi/models/AudioQuery.ts b/src/openapi/models/AudioQuery.ts index 8eeecebf6a..afb9854250 100644 --- a/src/openapi/models/AudioQuery.ts +++ b/src/openapi/models/AudioQuery.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/BaseLibraryInfo.ts b/src/openapi/models/BaseLibraryInfo.ts index 3f3c043048..8475c8948f 100644 --- a/src/openapi/models/BaseLibraryInfo.ts +++ b/src/openapi/models/BaseLibraryInfo.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/BodySingFrameF0SingFrameF0Post.ts b/src/openapi/models/BodySingFrameF0SingFrameF0Post.ts new file mode 100644 index 0000000000..601aabd7e4 --- /dev/null +++ b/src/openapi/models/BodySingFrameF0SingFrameF0Post.ts @@ -0,0 +1,88 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * DUMMY Engine + * DUMMY の音声合成エンジンです。 + * + * The version of the OpenAPI document: latest + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +import type { FrameAudioQuery } from './FrameAudioQuery'; +import { + FrameAudioQueryFromJSON, + FrameAudioQueryFromJSONTyped, + FrameAudioQueryToJSON, +} from './FrameAudioQuery'; +import type { Score } from './Score'; +import { + ScoreFromJSON, + ScoreFromJSONTyped, + ScoreToJSON, +} from './Score'; + +/** + * + * @export + * @interface BodySingFrameF0SingFrameF0Post + */ +export interface BodySingFrameF0SingFrameF0Post { + /** + * + * @type {Score} + * @memberof BodySingFrameF0SingFrameF0Post + */ + score: Score; + /** + * + * @type {FrameAudioQuery} + * @memberof BodySingFrameF0SingFrameF0Post + */ + frameAudioQuery: FrameAudioQuery; +} + +/** + * Check if a given object implements the BodySingFrameF0SingFrameF0Post interface. + */ +export function instanceOfBodySingFrameF0SingFrameF0Post(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "score" in value; + isInstance = isInstance && "frameAudioQuery" in value; + + return isInstance; +} + +export function BodySingFrameF0SingFrameF0PostFromJSON(json: any): BodySingFrameF0SingFrameF0Post { + return BodySingFrameF0SingFrameF0PostFromJSONTyped(json, false); +} + +export function BodySingFrameF0SingFrameF0PostFromJSONTyped(json: any, ignoreDiscriminator: boolean): BodySingFrameF0SingFrameF0Post { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'score': ScoreFromJSON(json['score']), + 'frameAudioQuery': FrameAudioQueryFromJSON(json['frame_audio_query']), + }; +} + +export function BodySingFrameF0SingFrameF0PostToJSON(value?: BodySingFrameF0SingFrameF0Post | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'score': ScoreToJSON(value.score), + 'frame_audio_query': FrameAudioQueryToJSON(value.frameAudioQuery), + }; +} + diff --git a/src/openapi/models/BodySingFrameVolumeSingFrameVolumePost.ts b/src/openapi/models/BodySingFrameVolumeSingFrameVolumePost.ts index 8b683c2abf..d4d5e6d644 100644 --- a/src/openapi/models/BodySingFrameVolumeSingFrameVolumePost.ts +++ b/src/openapi/models/BodySingFrameVolumeSingFrameVolumePost.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/CorsPolicyMode.ts b/src/openapi/models/CorsPolicyMode.ts index 73d3943c7f..701e15a556 100644 --- a/src/openapi/models/CorsPolicyMode.ts +++ b/src/openapi/models/CorsPolicyMode.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/DownloadableLibraryInfo.ts b/src/openapi/models/DownloadableLibraryInfo.ts index 07b88a16f3..4332804a89 100644 --- a/src/openapi/models/DownloadableLibraryInfo.ts +++ b/src/openapi/models/DownloadableLibraryInfo.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/EngineManifest.ts b/src/openapi/models/EngineManifest.ts index 7781b7a22b..476fe45437 100644 --- a/src/openapi/models/EngineManifest.ts +++ b/src/openapi/models/EngineManifest.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * @@ -111,7 +111,7 @@ export interface EngineManifest { */ supportedVvlibManifestVersion?: string; /** - * + * エンジンが持つ機能 * @type {SupportedFeatures} * @memberof EngineManifest */ diff --git a/src/openapi/models/FrameAudioQuery.ts b/src/openapi/models/FrameAudioQuery.ts index afa54d884d..eacc49b6a3 100644 --- a/src/openapi/models/FrameAudioQuery.ts +++ b/src/openapi/models/FrameAudioQuery.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/FramePhoneme.ts b/src/openapi/models/FramePhoneme.ts index 8606c28972..adbbefb736 100644 --- a/src/openapi/models/FramePhoneme.ts +++ b/src/openapi/models/FramePhoneme.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/HTTPValidationError.ts b/src/openapi/models/HTTPValidationError.ts index 29ec8b62fe..d7d9a5e0aa 100644 --- a/src/openapi/models/HTTPValidationError.ts +++ b/src/openapi/models/HTTPValidationError.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/InstalledLibraryInfo.ts b/src/openapi/models/InstalledLibraryInfo.ts index 81563a1fa5..9e25766419 100644 --- a/src/openapi/models/InstalledLibraryInfo.ts +++ b/src/openapi/models/InstalledLibraryInfo.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/LibrarySpeaker.ts b/src/openapi/models/LibrarySpeaker.ts index a24abf6443..b99b7b3481 100644 --- a/src/openapi/models/LibrarySpeaker.ts +++ b/src/openapi/models/LibrarySpeaker.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/LicenseInfo.ts b/src/openapi/models/LicenseInfo.ts index 99827e1029..0908112211 100644 --- a/src/openapi/models/LicenseInfo.ts +++ b/src/openapi/models/LicenseInfo.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/Mora.ts b/src/openapi/models/Mora.ts index 8a2699dcee..e00dc18727 100644 --- a/src/openapi/models/Mora.ts +++ b/src/openapi/models/Mora.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/MorphableTargetInfo.ts b/src/openapi/models/MorphableTargetInfo.ts index e743f8da15..da9cc66c81 100644 --- a/src/openapi/models/MorphableTargetInfo.ts +++ b/src/openapi/models/MorphableTargetInfo.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/Note.ts b/src/openapi/models/Note.ts index 1763b2eaf8..7dd33baa78 100644 --- a/src/openapi/models/Note.ts +++ b/src/openapi/models/Note.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/ParseKanaBadRequest.ts b/src/openapi/models/ParseKanaBadRequest.ts index 0d6e14557d..2373532ae4 100644 --- a/src/openapi/models/ParseKanaBadRequest.ts +++ b/src/openapi/models/ParseKanaBadRequest.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/Preset.ts b/src/openapi/models/Preset.ts index 78af2e078d..8a7a958c0d 100644 --- a/src/openapi/models/Preset.ts +++ b/src/openapi/models/Preset.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/Score.ts b/src/openapi/models/Score.ts index 0769a8585c..e9168cf080 100644 --- a/src/openapi/models/Score.ts +++ b/src/openapi/models/Score.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/Speaker.ts b/src/openapi/models/Speaker.ts index 9a5678b11e..840289e65c 100644 --- a/src/openapi/models/Speaker.ts +++ b/src/openapi/models/Speaker.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * @@ -57,7 +57,7 @@ export interface Speaker { */ version: string; /** - * + * キャラクターの対応機能 * @type {SpeakerSupportedFeatures} * @memberof Speaker */ diff --git a/src/openapi/models/SpeakerInfo.ts b/src/openapi/models/SpeakerInfo.ts index ec0da115cd..93cbcb0536 100644 --- a/src/openapi/models/SpeakerInfo.ts +++ b/src/openapi/models/SpeakerInfo.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/SpeakerStyle.ts b/src/openapi/models/SpeakerStyle.ts index dd46eb6fa8..377b5ecc74 100644 --- a/src/openapi/models/SpeakerStyle.ts +++ b/src/openapi/models/SpeakerStyle.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/SpeakerSupportedFeatures.ts b/src/openapi/models/SpeakerSupportedFeatures.ts index 63c35f021a..2f91cc8fd8 100644 --- a/src/openapi/models/SpeakerSupportedFeatures.ts +++ b/src/openapi/models/SpeakerSupportedFeatures.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/StyleInfo.ts b/src/openapi/models/StyleInfo.ts index 8333aec0f5..73cc19bd32 100644 --- a/src/openapi/models/StyleInfo.ts +++ b/src/openapi/models/StyleInfo.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/SupportedDevicesInfo.ts b/src/openapi/models/SupportedDevicesInfo.ts index db15302c39..c0facee0a2 100644 --- a/src/openapi/models/SupportedDevicesInfo.ts +++ b/src/openapi/models/SupportedDevicesInfo.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/SupportedFeatures.ts b/src/openapi/models/SupportedFeatures.ts index d68d63d050..5718d85838 100644 --- a/src/openapi/models/SupportedFeatures.ts +++ b/src/openapi/models/SupportedFeatures.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/UpdateInfo.ts b/src/openapi/models/UpdateInfo.ts index 8949a09e3d..3e39392379 100644 --- a/src/openapi/models/UpdateInfo.ts +++ b/src/openapi/models/UpdateInfo.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/UserDictWord.ts b/src/openapi/models/UserDictWord.ts index a08d572975..dcbb4299d6 100644 --- a/src/openapi/models/UserDictWord.ts +++ b/src/openapi/models/UserDictWord.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/ValidationError.ts b/src/openapi/models/ValidationError.ts index a08053b021..e893a2d602 100644 --- a/src/openapi/models/ValidationError.ts +++ b/src/openapi/models/ValidationError.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/ValidationErrorLocInner.ts b/src/openapi/models/ValidationErrorLocInner.ts index db6095c134..18a47618e5 100644 --- a/src/openapi/models/ValidationErrorLocInner.ts +++ b/src/openapi/models/ValidationErrorLocInner.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/VvlibManifest.ts b/src/openapi/models/VvlibManifest.ts index 4ed628ebc8..c65a160629 100644 --- a/src/openapi/models/VvlibManifest.ts +++ b/src/openapi/models/VvlibManifest.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/WordTypes.ts b/src/openapi/models/WordTypes.ts index 78a3d901e8..4d026652a7 100644 --- a/src/openapi/models/WordTypes.ts +++ b/src/openapi/models/WordTypes.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/index.ts b/src/openapi/models/index.ts index 5ed82691a4..19164338dc 100644 --- a/src/openapi/models/index.ts +++ b/src/openapi/models/index.ts @@ -3,6 +3,7 @@ export * from './AccentPhrase'; export * from './AudioQuery'; export * from './BaseLibraryInfo'; +export * from './BodySingFrameF0SingFrameF0Post'; export * from './BodySingFrameVolumeSingFrameVolumePost'; export * from './CorsPolicyMode'; export * from './DownloadableLibraryInfo'; diff --git a/src/openapi/runtime.ts b/src/openapi/runtime.ts index f982393c8d..84b3765c73 100644 --- a/src/openapi/runtime.ts +++ b/src/openapi/runtime.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * From 4954e9a6bbb21c35e4d34c266f19dce445a7bb7b Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Fri, 3 Jan 2025 20:12:05 +0900 Subject: [PATCH 12/23] =?UTF-8?q?fix:=20VVPP=E3=83=87=E3=83=95=E3=82=A9?= =?UTF-8?q?=E3=83=AB=E3=83=88=E3=82=A8=E3=83=B3=E3=82=B8=E3=83=B3=E3=83=80?= =?UTF-8?q?=E3=82=A6=E3=83=B3=E3=83=AD=E3=83=BC=E3=83=89=E5=BE=8C=E3=80=81?= =?UTF-8?q?=E7=A2=BA=E5=AE=9F=E3=81=AB=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=81=8C=E9=96=89=E3=81=98=E3=82=89=E3=82=8C=E3=82=8B=E3=81=BE?= =?UTF-8?q?=E3=81=A7=E5=BE=85=E3=81=A4=20(#2466)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/electron/engineAndVvppController.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/backend/electron/engineAndVvppController.ts b/src/backend/electron/engineAndVvppController.ts index cc4bd95952..c9342818d5 100644 --- a/src/backend/electron/engineAndVvppController.ts +++ b/src/backend/electron/engineAndVvppController.ts @@ -230,7 +230,13 @@ export class EngineAndVvppController { progress: (downloadedBytes / totalBytes) * 100, }); } + + // ファイルを確実に閉じる + const { promise, resolve, reject } = Promise.withResolvers(); + fileStream.on("close", resolve); + fileStream.on("error", reject); fileStream.close(); + await promise; downloadedPaths.push(downloadPath); log.info(`Downloaded ${name} to ${downloadPath}`); From f94463cc7165a7ec21ce8c5c028c04206cc810b4 Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Fri, 3 Jan 2025 21:31:10 +0900 Subject: [PATCH 13/23] =?UTF-8?q?fix:=20electron=E3=81=A7=E3=83=97?= =?UTF-8?q?=E3=83=AD=E3=82=BB=E3=82=B9=E3=81=AE=E7=92=B0=E5=A2=83=E5=A4=89?= =?UTF-8?q?=E6=95=B0=E3=81=8C=E3=81=AA=E3=81=84=E5=A0=B4=E5=90=88=E3=81=AB?= =?UTF-8?q?=E8=90=BD=E3=81=A1=E3=81=AA=E3=81=84=E3=82=88=E3=81=86=E3=81=AB?= =?UTF-8?q?=20(#2461)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nanashi. --- src/domain/defaultEngine/envEngineInfo.ts | 26 +++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/domain/defaultEngine/envEngineInfo.ts b/src/domain/defaultEngine/envEngineInfo.ts index 4feee23d5c..7aabe35227 100644 --- a/src/domain/defaultEngine/envEngineInfo.ts +++ b/src/domain/defaultEngine/envEngineInfo.ts @@ -33,14 +33,28 @@ const envEngineInfoSchema = z ); type EnvEngineInfoType = z.infer; +/** + * デフォルトエンジン情報の環境変数を取得する + * electronのときはプロセスの環境変数を優先する。 + * NOTE: electronテスト環境を切り替えるため。テスト環境が1本化されればimport.meta.envを使う。 + */ +function getDefaultEngineInfosEnv(): string { + let engineInfos; + if (isElectron) { + engineInfos = process?.env?.VITE_DEFAULT_ENGINE_INFOS; + } + if (engineInfos == undefined) { + engineInfos = import.meta.env.VITE_DEFAULT_ENGINE_INFOS; + } + if (engineInfos == undefined) { + engineInfos = "[]"; + } + return engineInfos; +} + /** .envからデフォルトエンジン情報を読み込む */ export function loadEnvEngineInfos(): EnvEngineInfoType[] { - // electronのときはプロセスの環境変数を参照する。 - // NOTE: electronテスト環境を切り替えるため。テスト環境が1本化されればimport.meta.envを使う。 - const defaultEngineInfosEnv = - (isElectron - ? process.env.VITE_DEFAULT_ENGINE_INFOS - : import.meta.env.VITE_DEFAULT_ENGINE_INFOS) ?? "[]"; + const defaultEngineInfosEnv = getDefaultEngineInfosEnv(); // FIXME: 「.envを書き換えてください」というログを出したい // NOTE: domainディレクトリなのでログを出す方法がなく、Errorオプションのcauseを用いてもelectron-logがcauseのログを出してくれない From 438d8dc3fc9242bc562b0c3858ed9b99d7f0a9d1 Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Fri, 3 Jan 2025 21:36:00 +0900 Subject: [PATCH 14/23] =?UTF-8?q?refactor:=20=E3=83=80=E3=82=A6=E3=83=B3?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=83=89=E3=81=97=E3=81=9F=E3=82=A8=E3=83=B3?= =?UTF-8?q?=E3=82=B8=E3=83=B3=E3=81=AErun=E3=81=AE=E6=A8=A9=E9=99=90?= =?UTF-8?q?=E3=82=92=E5=A4=89=E6=9B=B4=E3=81=99=E3=82=8B=E9=83=A8=E5=88=86?= =?UTF-8?q?=E3=81=AE=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF=E3=83=AA?= =?UTF-8?q?=E3=83=B3=E3=82=B0=20(#2467)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/download-engine/action.yml | 7 +++++++ .github/workflows/test.yml | 3 --- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/actions/download-engine/action.yml b/.github/actions/download-engine/action.yml index 081dcc917b..fc4af5eaba 100644 --- a/.github/actions/download-engine/action.yml +++ b/.github/actions/download-engine/action.yml @@ -85,6 +85,13 @@ runs: mkdir -p $DEST mv $TEMPDIR/tmp-extract/$TARGET/* $DEST + # 実行ファイルのパーミッションを変更 + if [ "${{ runner.os }}" = "Windows" ]; then + chmod +x $DEST/run.exe + else + chmod +x $DEST/run + fi + echo "::group::ll $DEST" ls -al $DEST echo "::endgroup::" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b294b3b879..47df09f071 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -102,9 +102,6 @@ jobs: - name: Setup run: | - # run.exe - chmod +x ${{ steps.download-engine.outputs.run_path }} - # .env sed -i -e 's|"074fc39e-678b-4c13-8916-ffca8d505d1d"|"208cf94d-43d2-4cf5-abc0-9783cac36d29"|' .env.test sed -i -e 's|"../voicevox_engine/run.exe"|"${{ steps.download-engine.outputs.run_path }}"|' .env.test From 8a6d2a4e610f80e08b7475c3394725b058bb60ad Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Fri, 3 Jan 2025 21:52:12 +0900 Subject: [PATCH 15/23] =?UTF-8?q?fix:=20=E5=8D=98=E8=AA=9E=E3=81=AE?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E3=81=8A=E3=82=88=E3=81=B3=E7=99=BB=E9=8C=B2?= =?UTF-8?q?=E6=99=82=E3=81=AE=E4=BE=8B=E5=A4=96=E3=82=92=E3=82=B9=E3=83=AD?= =?UTF-8?q?=E3=83=BC=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=20(#2470)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Dialog/DictionaryEditWordDialog.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Dialog/DictionaryEditWordDialog.vue b/src/components/Dialog/DictionaryEditWordDialog.vue index 5d51cfa6bf..7b916a9bdc 100644 --- a/src/components/Dialog/DictionaryEditWordDialog.vue +++ b/src/components/Dialog/DictionaryEditWordDialog.vue @@ -296,12 +296,12 @@ const saveWord = async () => { accentType: accent, priority: wordPriority.value, }); - } catch { + } catch (e) { void store.actions.SHOW_ALERT_DIALOG({ title: "単語の更新に失敗しました", message: "エンジンの再起動をお試しください。", }); - return; + throw e; } } else { try { @@ -313,12 +313,12 @@ const saveWord = async () => { priority: wordPriority.value, }), ); - } catch { + } catch (e) { void store.actions.SHOW_ALERT_DIALOG({ title: "単語の登録に失敗しました", message: "エンジンの再起動をお試しください。", }); - return; + throw e; } } await loadingDictProcess(); From faf91f03a297b261db2e3b2404a5736f70c3443e Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Fri, 3 Jan 2025 22:52:29 +0900 Subject: [PATCH 16/23] =?UTF-8?q?refactor:=20electron=E7=94=A8=E3=81=AE.en?= =?UTF-8?q?v=E3=82=92=E4=BD=9C=E6=88=90=E3=81=97=E3=80=81test.yml=E3=82=92?= =?UTF-8?q?=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3?= =?UTF-8?q?=E3=82=B0=20(#2468)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 9 ++++----- tests/e2e/electron/example.spec.ts | 2 +- tests/env/.env.test-e2e | 16 ++++++++++++++++ .../env/.env.test-electron-default-vvpp | 0 4 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 tests/env/.env.test-e2e rename .env.test-electron-default-vvpp => tests/env/.env.test-electron-default-vvpp (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 47df09f071..1b3fb1cd02 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -103,13 +103,12 @@ jobs: - name: Setup run: | # .env - sed -i -e 's|"074fc39e-678b-4c13-8916-ffca8d505d1d"|"208cf94d-43d2-4cf5-abc0-9783cac36d29"|' .env.test - sed -i -e 's|"../voicevox_engine/run.exe"|"${{ steps.download-engine.outputs.run_path }}"|' .env.test + cp tests/env/.env.test-e2e .env + sed -i -e 's|"path/to/engine"|"${{ steps.download-engine.outputs.run_path }}"|' .env # GitHub Actions 環境だとたまに50021が封じられていることがあるので、ランダムなポートを使うようにする PORT=$(node -r net -e "server=net.createServer();server.listen(0,()=>{console.log(server.address().port);server.close()})") - sed -i -e 's|"host": "http://127.0.0.1:50021"|"host": "http://127.0.0.1:'$PORT'"|' .env.test - sed -i -e 's|"executionArgs": \[\],|"executionArgs": ["--port='$PORT'"],|' .env.test - cp .env.test .env + sed -i -e 's|random_port|'$PORT'|' .env + cat .env # ログ用 - name: Run npm run test:browser-e2e run: | diff --git a/tests/e2e/electron/example.spec.ts b/tests/e2e/electron/example.spec.ts index 1c0ee5dbe7..0795b258ef 100644 --- a/tests/e2e/electron/example.spec.ts +++ b/tests/e2e/electron/example.spec.ts @@ -46,7 +46,7 @@ test.beforeEach(async () => { }, { envName: "VVPPデフォルトエンジン", - envPath: ".env.test-electron-default-vvpp", + envPath: "tests/env/.env.test-electron-default-vvpp", }, ].forEach(({ envName, envPath }) => { test.describe(`${envName}`, () => { diff --git a/tests/env/.env.test-e2e b/tests/env/.env.test-e2e new file mode 100644 index 0000000000..0cf137dae0 --- /dev/null +++ b/tests/env/.env.test-e2e @@ -0,0 +1,16 @@ +# CI環境でのe2eテスト用の.envファイル。CI時に値が上書きされる。 + +VITE_APP_NAME=voicevox +VITE_DEFAULT_ENGINE_INFOS=`[ + { + "name": "VOICEVOX Nemo Engine", + "uuid": "208cf94d-43d2-4cf5-abc0-9783cac36d29", + "executionEnabled": true, + "executionFilePath": "path/to/engine", + "executionArgs": ["--port=random_port"], + "host": "http://127.0.0.1:random_port" + } +]` +VITE_OFFICIAL_WEBSITE_URL=https://voicevox.hiroshiba.jp/ +VITE_LATEST_UPDATE_INFOS_URL=https://voicevox.hiroshiba.jp/updateInfos.json +VITE_GTM_CONTAINER_ID=GTM-DUMMY diff --git a/.env.test-electron-default-vvpp b/tests/env/.env.test-electron-default-vvpp similarity index 100% rename from .env.test-electron-default-vvpp rename to tests/env/.env.test-electron-default-vvpp From 660822d32023a6b0863214374fe7b019b993eee3 Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Fri, 3 Jan 2025 22:55:12 +0900 Subject: [PATCH 17/23] =?UTF-8?q?fix:=20=E3=82=A8=E3=83=B3=E3=82=B8?= =?UTF-8?q?=E3=83=B3=E3=83=A2=E3=83=83=E3=82=AF=E5=86=85=E3=81=A7=E3=81=AE?= =?UTF-8?q?=E8=AA=AD=E7=82=B9=E3=81=AE=E6=89=B1=E3=81=84=E3=81=8C=E3=81=A1?= =?UTF-8?q?=E3=82=87=E3=81=A3=E3=81=A8=E9=96=93=E9=81=95=E3=81=A3=E3=81=A6?= =?UTF-8?q?=E3=81=84=E3=81=9F=E3=81=AE=E3=81=A7=E4=BF=AE=E6=AD=A3=20(#2469?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mock/engineMock/talkModelMock.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/mock/engineMock/talkModelMock.ts b/src/mock/engineMock/talkModelMock.ts index 016dbc30e7..cb55943a6d 100644 --- a/src/mock/engineMock/talkModelMock.ts +++ b/src/mock/engineMock/talkModelMock.ts @@ -182,16 +182,23 @@ export async function textToActtentPhrasesMock(text: string, styleId: number) { for (const token of tokens) { // 記号の場合は無音を入れて区切る if (token.pos == "記号") { - if (textPhrase.length == 0) continue; - - const accentPhrase = textToAccentPhraseMock(textPhrase); - accentPhrase.pauseMora = { + const pauseMora = { text: "、", vowel: "pau", vowelLength: 1 - 1 / (accentPhrases.length + 1), pitch: 0, }; - accentPhrases.push(accentPhrase); + + // テキストが空の場合は前のアクセント句に無音を追加、空でない場合は新しいアクセント句を追加 + let accentPhrase: AccentPhrase; + if (textPhrase.length === 0) { + accentPhrase = accentPhrases[accentPhrases.length - 1]; + } else { + accentPhrase = textToAccentPhraseMock(textPhrase); + accentPhrases.push(accentPhrase); + } + accentPhrase.pauseMora = pauseMora; + textPhrase = ""; continue; } From f4bcc6e10450a7ceb28aedf2fdbe8c16d07254f6 Mon Sep 17 00:00:00 2001 From: "Nanashi." Date: Sat, 4 Jan 2025 09:09:41 +0900 Subject: [PATCH 18/23] =?UTF-8?q?fix:=20state=E3=81=A7=E5=9E=8B=E3=82=A8?= =?UTF-8?q?=E3=83=A9=E3=83=BC=E3=81=8C=E5=87=BA=E3=82=8B=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB=E3=81=99=E3=82=8B=20(#2439)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Hiroshiba --- src/helpers/typedEntries.ts | 15 +++++++++++++++ src/store/command.ts | 7 ++++--- src/store/setting.ts | 8 ++++---- src/store/singing.ts | 12 ------------ src/store/type.ts | 9 ++------- src/store/ui.ts | 8 ++++++-- 6 files changed, 31 insertions(+), 28 deletions(-) create mode 100644 src/helpers/typedEntries.ts diff --git a/src/helpers/typedEntries.ts b/src/helpers/typedEntries.ts new file mode 100644 index 0000000000..a8a07c115b --- /dev/null +++ b/src/helpers/typedEntries.ts @@ -0,0 +1,15 @@ +/** 型付きのObject.entries */ +export const objectEntries = >( + obj: T, +): [keyof T, T[keyof T]][] => { + return Object.entries(obj) as [keyof T, T[keyof T]][]; +}; + +/** 型付きのObject.fromEntries */ +export const objectFromEntries = < + const T extends ReadonlyArray, +>( + entries: T, +): { [K in T[number] as K[0]]: K[1] } => { + return Object.fromEntries(entries) as { [K in T[number] as K[0]]: K[1] }; +}; diff --git a/src/store/command.ts b/src/store/command.ts index ecb5b37656..3d5ee0cf57 100644 --- a/src/store/command.ts +++ b/src/store/command.ts @@ -11,6 +11,7 @@ import { } from "@/store/vuex"; import { CommandId, EditorType } from "@/type/preload"; import { uuid4 } from "@/helpers/random"; +import { objectEntries, objectFromEntries } from "@/helpers/typedEntries"; enablePatches(); enableMapSet(); @@ -32,10 +33,10 @@ export const createCommandMutationTree = ( payloadRecipeTree: PayloadRecipeTree, editor: EditorType, ): MutationTree => - Object.fromEntries( - Object.entries(payloadRecipeTree).map(([key, val]) => [ + objectFromEntries( + objectEntries(payloadRecipeTree).map(([key, val]) => [ key, - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + // @ts-expect-error とりあえず動くので無視 createCommandMutation(val, editor), ]), ) as MutationTree; diff --git a/src/store/setting.ts b/src/store/setting.ts index 6f14d6c9af..e000302461 100644 --- a/src/store/setting.ts +++ b/src/store/setting.ts @@ -218,15 +218,15 @@ export const settingStore = createPartialStore({ SET_ROOT_MISC_SETTING: { mutation(state, { key, value }) { - // Vuexの型処理でUnionが解かれてしまうのを迂回している + // @ts-expect-error Vuexの型処理でUnionが解かれてしまうのを迂回している // FIXME: このワークアラウンドをなくす - state[key as never] = value; + state[key] = value; }, action({ mutations }, { key, value }) { void window.backend.setSetting(key, value); - // Vuexの型処理でUnionが解かれてしまうのを迂回している + // @ts-expect-error Vuexの型処理でUnionが解かれてしまうのを迂回している // FIXME: このワークアラウンドをなくす - mutations.SET_ROOT_MISC_SETTING({ key: key as never, value }); + mutations.SET_ROOT_MISC_SETTING({ key, value }); }, }, diff --git a/src/store/singing.ts b/src/store/singing.ts index 3e18f02736..ed15a1759c 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -1552,18 +1552,6 @@ export const singingStore = createPartialStore({ }, }, - SET_IS_DRAG: { - mutation(state, { isDrag }: { isDrag: boolean }) { - // FIXME: state.isDragが無くなっているので修正する - state.isDrag = isDrag; - }, - async action({ mutations }, { isDrag }) { - mutations.SET_IS_DRAG({ - isDrag, - }); - }, - }, - SET_START_RENDERING_REQUESTED: { mutation(state, { startRenderingRequested }) { state.startRenderingRequested = startRenderingRequested; diff --git a/src/store/type.ts b/src/store/type.ts index 53c1081107..1e83ab6a0b 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -1151,11 +1151,6 @@ export type SingingStoreTypes = { action(payload: { sequencerPitchTool: PitchEditTool }): void; }; - SET_IS_DRAG: { - mutation: { isDrag: boolean }; - action(payload: { isDrag: boolean }): void; - }; - EXPORT_LABEL_FILES: { action(payload: { dirPath?: string }): SaveResultObject[]; }; @@ -2272,7 +2267,7 @@ export type PresetStoreTypes = { * Dictionary Store Types */ -export type DictionaryStoreState = Record; +export type DictionaryStoreState = Record; export type DictionaryStoreTypes = { LOAD_USER_DICT: { @@ -2312,7 +2307,7 @@ export type DictionaryStoreTypes = { * Setting Store Types */ -export type ProxyStoreState = Record; +export type ProxyStoreState = Record; export type IEngineConnectorFactoryActions = ReturnType< IEngineConnectorFactory["instance"] diff --git a/src/store/ui.ts b/src/store/ui.ts index 76558dbcf6..14eab4b367 100644 --- a/src/store/ui.ts +++ b/src/store/ui.ts @@ -30,6 +30,7 @@ import { showNotifyAndNotShowAgainButton, showWarningDialog, } from "@/components/Dialog/Dialog"; +import { objectEntries } from "@/helpers/typedEntries"; export function createUILockAction( action: ( @@ -177,15 +178,18 @@ export const uiStore = createPartialStore({ SET_DIALOG_OPEN: { mutation(state, dialogState) { - for (const [key, value] of Object.entries(dialogState)) { + for (const [key, value] of objectEntries(dialogState)) { if (!(key in state)) { throw new Error(`Unknown dialog state: ${key}`); } + if (value == undefined) { + throw new Error(`Invalid dialog state: ${key}`); + } state[key] = value; } }, async action({ state, mutations }, dialogState) { - for (const [key, value] of Object.entries(dialogState)) { + for (const [key, value] of objectEntries(dialogState)) { if (state[key] === value) continue; if (value) { From 4c3204840bd028bc54734dc6816e85293ccb6fb1 Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Sat, 4 Jan 2025 09:13:54 +0900 Subject: [PATCH 19/23] =?UTF-8?q?refactor(test):=20=E8=BE=9E=E6=9B=B8?= =?UTF-8?q?=E3=83=80=E3=82=A4=E3=82=A2=E3=83=AD=E3=82=B0=E3=81=AE=E8=AA=AD?= =?UTF-8?q?=E3=81=BF=E6=96=B9=E5=8F=96=E5=BE=97=E3=83=AD=E3=82=B8=E3=83=83?= =?UTF-8?q?=E3=82=AF=E3=82=92=E6=94=B9=E5=96=84=20(#2471)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...3\202\242\343\203\255\343\202\260.spec.ts" | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git "a/tests/e2e/browser/\350\276\236\346\233\270\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts" "b/tests/e2e/browser/\350\276\236\346\233\270\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts" index 8ab1694115..9d8df8039b 100644 --- "a/tests/e2e/browser/\350\276\236\346\233\270\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts" +++ "b/tests/e2e/browser/\350\276\236\346\233\270\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts" @@ -4,29 +4,27 @@ import { getNewestQuasarDialog } from "../locators"; test.beforeEach(gotoHome); -// 読み方を確認する。 -// エンジン起動直後など、たまに読みが反映されないことがあるので、 -// 一度空にする -> テキストが消えたことを確認(消えてなかったらもう一度Enter)-> -// 再度入力する -> 読み方が表示されたことを確認(表示されてなかったらもう一度Enter) -// という流れで読み方を確認する。 +/** + * 最後のテキスト欄にテキストを入力し、その読みを取得する。 + * 確実に読みを反映させるために、一度空にしてから入力する。 + */ async function getYomi(page: Page, inputText: string): Promise { - const audioCellInput = page.locator(".audio-cell input").last(); + const audioCellInput = page.getByRole("textbox", { name: "行目" }).last(); + const accentPhrase = page.locator(".accent-phrase"); + + // 空にする + await audioCellInput.click(); await audioCellInput.fill(""); - let text = ""; - do { - await page.waitForTimeout(100); - await audioCellInput.press("Enter"); - text = (await page.locator(".text-cell").allInnerTexts()).join(""); - } while (text.length > 0); + await audioCellInput.press("Enter"); + await expect(accentPhrase).not.toBeVisible(); + // 入力する + await audioCellInput.click(); await audioCellInput.fill(inputText); - do { - await page.waitForTimeout(100); - await audioCellInput.press("Enter"); - text = (await page.locator(".text-cell").allInnerTexts()).join(""); - } while (text.length === 0); + await audioCellInput.press("Enter"); + await expect(accentPhrase).not.toHaveCount(0); - return text; + return (await accentPhrase.allTextContents()).join(""); } async function openDictDialog(page: Page): Promise { From 11c2e1644c7960133ba1ff47ce05e6f677bf3b5e Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Sun, 5 Jan 2025 00:02:02 +0900 Subject: [PATCH 20/23] =?UTF-8?q?refactor:=20browser:e2e=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=82=92=E3=83=A2=E3=83=83=E3=82=AF=E7=94=A8=E3=82=A8?= =?UTF-8?q?=E3=83=B3=E3=82=B8=E3=83=B3=E3=82=92=E4=BD=BF=E3=81=86=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E5=A4=89=E6=9B=B4=20(#2442)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.test | 12 +- .github/workflows/test.yml | 20 +-- playwright.config.ts | 35 ++--- src/infrastructures/EngineConnector.ts | 44 ++++++- src/store/proxy.ts | 11 +- ...3\202\273\343\203\263\343\203\210.spec.ts" | 1 - ...3\203\247\343\203\203\343\203\210.spec.ts" | 124 ------------------ ...347\224\273\351\235\242-browser-win32.png" | Bin 31607 -> 41222 bytes ...347\224\273\351\235\242-browser-win32.png" | Bin 47948 -> 47604 bytes ...3\202\242\343\203\255\343\202\260.spec.ts" | 32 ++--- ...5\243\260\350\251\263\347\264\260.spec.ts" | 38 +++--- .../env/{.env.test-e2e => .env.test-electron} | 2 +- .../__snapshots__/configManager.spec.ts.snap | 2 +- 13 files changed, 113 insertions(+), 208 deletions(-) rename tests/env/{.env.test-e2e => .env.test-electron} (84%) diff --git a/.env.test b/.env.test index ab77dc1307..014c32203d 100644 --- a/.env.test +++ b/.env.test @@ -1,12 +1,14 @@ +# テスト用の.envファイル。モックを使う。 + VITE_APP_NAME=voicevox VITE_DEFAULT_ENGINE_INFOS=`[ { - "uuid": "074fc39e-678b-4c13-8916-ffca8d505d1d", - "name": "VOICEVOX Engine", - "executionEnabled": true, - "executionFilePath": "../voicevox_engine/run.exe", + "name": "Mock Engine", + "uuid": "00000000-0000-0000-0000-000000000000", + "executionEnabled": false, + "executionFilePath": "dummy/path", "executionArgs": [], - "host": "http://127.0.0.1:50021" + "host": "mock://mock" } ]` VITE_OFFICIAL_WEBSITE_URL=https://voicevox.hiroshiba.jp/ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1b3fb1cd02..b06a4b0dd0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -100,16 +100,6 @@ jobs: dest: ${{ github.workspace }}/voicevox_engine target: ${{ matrix.voicevox_engine_asset_name }} - - name: Setup - run: | - # .env - cp tests/env/.env.test-e2e .env - sed -i -e 's|"path/to/engine"|"${{ steps.download-engine.outputs.run_path }}"|' .env - # GitHub Actions 環境だとたまに50021が封じられていることがあるので、ランダムなポートを使うようにする - PORT=$(node -r net -e "server=net.createServer();server.listen(0,()=>{console.log(server.address().port);server.close()})") - sed -i -e 's|random_port|'$PORT'|' .env - cat .env # ログ用 - - name: Run npm run test:browser-e2e run: | if [ -n "${{ runner.debug }}" ]; then @@ -123,6 +113,14 @@ jobs: - name: Run npm run test:electron-e2e run: | + # .env + cp tests/env/.env.test-electron .env + sed -i -e 's|"path/to/engine"|"${{ steps.download-engine.outputs.run_path }}"|' .env + # GitHub Actions 環境だとたまに50021が封じられていることがあるので、ランダムなポートを使うようにする + PORT=$(node -r net -e "server=net.createServer();server.listen(0,()=>{console.log(server.address().port);server.close()})") + sed -i -e 's|random_port|'$PORT'|' .env + cat .env # ログ用 + if [ -n "${{ runner.debug }}" ]; then export DEBUG="pw:browser*" fi @@ -132,6 +130,8 @@ jobs: npm run test:electron-e2e fi + rm .env + - name: Run npm run test:storybook-vrt run: | if [ -n "${{ runner.debug }}" ]; then diff --git a/playwright.config.ts b/playwright.config.ts index 155d4e8b75..529bf25210 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,8 +1,15 @@ -import type { PlaywrightTestConfig, Project } from "@playwright/test"; -import { z } from "zod"; +/** + * e2eテストと .env の設計: + * - デフォルトで .env.test を読み込む。 + * モックエンジンが使われる。 + * - Electronテストはテストファイル内で様々な .env を読み込む。 + * テスト条件によって用意したい環境が異なるため。 + */ +import type { PlaywrightTestConfig, Project } from "@playwright/test"; import dotenv from "dotenv"; -dotenv.config({ override: true }); + +dotenv.config({ path: ".env.test", override: true }); let project: Project; let webServers: PlaywrightTestConfig["webServer"]; @@ -10,26 +17,6 @@ const isElectron = process.env.VITE_TARGET === "electron"; const isBrowser = process.env.VITE_TARGET === "browser"; const isStorybook = process.env.TARGET === "storybook"; -// エンジンの起動が必要 -const defaultEngineInfosEnv = process.env.VITE_DEFAULT_ENGINE_INFOS ?? "[]"; -const envSchema = z // FIXME: electron起動時のものと共通化したい - .object({ - host: z.string(), - executionFilePath: z.string(), - executionArgs: z.array(z.string()), - executionEnabled: z.boolean(), - }) - .passthrough() - .array(); -const engineInfos = envSchema.parse(JSON.parse(defaultEngineInfosEnv)); - -const engineServers = engineInfos - .filter((info) => info.executionEnabled) - .map((info) => ({ - command: `${info.executionFilePath} ${info.executionArgs.join(" ")}`, - url: `${info.host}/version`, - reuseExistingServer: !process.env.CI, - })); const viteServer = { command: "vite --mode test --port 7357", port: 7357, @@ -46,7 +33,7 @@ if (isElectron) { webServers = [viteServer]; } else if (isBrowser) { project = { name: "browser", testDir: "./tests/e2e/browser" }; - webServers = [viteServer, ...engineServers]; + webServers = [viteServer]; } else if (isStorybook) { project = { name: "storybook", testDir: "./tests/e2e/storybook" }; webServers = [storybookServer]; diff --git a/src/infrastructures/EngineConnector.ts b/src/infrastructures/EngineConnector.ts index 7ddede8273..be2870b803 100644 --- a/src/infrastructures/EngineConnector.ts +++ b/src/infrastructures/EngineConnector.ts @@ -1,3 +1,5 @@ +import { createEngineUrl, EngineUrlParams } from "@/domain/url"; +import { createOpenAPIEngineMock } from "@/mock/engineMock"; import { Configuration, DefaultApi, DefaultApiInterface } from "@/openapi"; export interface IEngineConnectorFactory { @@ -6,6 +8,7 @@ export interface IEngineConnectorFactory { instance: (host: string) => DefaultApiInterface; } +// 通常エンジン const OpenAPIEngineConnectorFactoryImpl = (): IEngineConnectorFactory => { const instanceMapper: Record = {}; return { @@ -21,6 +24,45 @@ const OpenAPIEngineConnectorFactoryImpl = (): IEngineConnectorFactory => { }, }; }; - export const OpenAPIEngineConnectorFactory = OpenAPIEngineConnectorFactoryImpl(); + +// モック用エンジン +const OpenAPIMockEngineConnectorFactoryImpl = (): IEngineConnectorFactory => { + let mockInstance: DefaultApiInterface | undefined; + return { + instance: () => { + if (!mockInstance) { + mockInstance = createOpenAPIEngineMock(); + } + return mockInstance; + }, + }; +}; +export const OpenAPIMockEngineConnectorFactory = + OpenAPIMockEngineConnectorFactoryImpl(); + +// 通常エンジンとモック用エンジンの両対応 +// モック用エンジンのURLのときはモックを、そうじゃないときは通常エンジンを返す。 +const OpenAPIEngineAndMockConnectorFactoryImpl = + (): IEngineConnectorFactory => { + // モック用エンジンのURLは `mock://mock` とする + const mockUrlParams: EngineUrlParams = { + protocol: "mock:", + hostname: "mock", + port: "", + pathname: "", + }; + + return { + instance: (host: string) => { + if (host == createEngineUrl(mockUrlParams)) { + return OpenAPIMockEngineConnectorFactory.instance(host); + } else { + return OpenAPIEngineConnectorFactory.instance(host); + } + }, + }; + }; +export const OpenAPIEngineAndMockConnectorFactory = + OpenAPIEngineAndMockConnectorFactoryImpl(); diff --git a/src/store/proxy.ts b/src/store/proxy.ts index 283e2e9fd2..0873c974a1 100644 --- a/src/store/proxy.ts +++ b/src/store/proxy.ts @@ -1,8 +1,10 @@ import { ProxyStoreState, ProxyStoreTypes, EditorAudioQuery } from "./type"; import { createPartialStore } from "./vuex"; import { createEngineUrl } from "@/domain/url"; +import { isElectron, isProduction } from "@/helpers/platform"; import { IEngineConnectorFactory, + OpenAPIEngineAndMockConnectorFactory, OpenAPIEngineConnectorFactory, } from "@/infrastructures/EngineConnector"; import { AudioQuery } from "@/openapi"; @@ -69,4 +71,11 @@ export const convertAudioQueryFromEngineToEditor = ( }; }; -export const proxyStore = proxyStoreCreator(OpenAPIEngineConnectorFactory); +// 製品PC版は通常エンジンのみを、それ以外はモックエンジンも使えるようする +const getConnectorFactory = () => { + if (isElectron && isProduction) { + return OpenAPIEngineConnectorFactory; + } + return OpenAPIEngineAndMockConnectorFactory; +}; +export const proxyStore = proxyStoreCreator(getConnectorFactory()); diff --git "a/tests/e2e/browser/\343\202\242\343\202\257\343\202\273\343\203\263\343\203\210.spec.ts" "b/tests/e2e/browser/\343\202\242\343\202\257\343\202\273\343\203\263\343\203\210.spec.ts" index b18d149670..02a611fee7 100644 --- "a/tests/e2e/browser/\343\202\242\343\202\257\343\202\273\343\203\263\343\203\210.spec.ts" +++ "b/tests/e2e/browser/\343\202\242\343\202\257\343\202\273\343\203\263\343\203\210.spec.ts" @@ -30,7 +30,6 @@ test("アクセントの読み部分をクリックすると読みを変更で await expect(page.locator(".text-cell").first()).toBeVisible(); await page.locator(".text-cell").first().click(); const input = page.getByLabel("1番目のアクセント区間の読み"); - await input.evaluate((node) => console.log(node.outerHTML)); expect(await input.inputValue()).toBe("テストデス"); await input.fill("テストテスト"); await input.press("Enter"); diff --git "a/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts" "b/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts" index 485eb7fb66..37196f334a 100644 --- "a/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts" +++ "b/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts" @@ -1,130 +1,6 @@ -import path from "path"; -import fs from "fs/promises"; import { test, expect } from "@playwright/test"; import { gotoHome, navigateToMain } from "../navigators"; -import { - Speaker, - SpeakerFromJSON, - SpeakerInfo, - SpeakerInfoFromJSON, - SpeakerInfoToJSON, - SpeakerToJSON, -} from "@/openapi"; -let speakerImages: - | { - portrait: string; - icon: string; - }[] - | undefined = undefined; - -/** - * 差し替え用の立ち絵・アイコンを取得する。 - * TODO: エンジンモックを使ってこのコードを削除する。 - */ -async function getSpeakerImages(): Promise< - { - portrait: string; - icon: string; - }[] -> { - if (!speakerImages) { - const assetsPath = path.resolve( - __dirname, - "../../../src/mock/engineMock/assets", - ); - const images = await fs.readdir(assetsPath); - const icons = images.filter((image) => image.startsWith("icon")); - icons.sort( - (a, b) => - parseInt(a.split(".")[0].split("_")[1]) - - parseInt(b.split(".")[0].split("_")[1]), - ); - speakerImages = await Promise.all( - icons.map(async (iconPath) => { - const portraitPath = iconPath.replace("icon_", "portrait_"); - const portrait = await fs.readFile( - path.join(assetsPath, portraitPath), - "base64", - ); - const icon = await fs.readFile( - path.join(assetsPath, iconPath), - "base64", - ); - - return { portrait, icon }; - }), - ); - } - return speakerImages; -} - -test.beforeEach(async ({ page }) => { - let speakers: Speaker[]; - const speakerImages = await getSpeakerImages(); - // Voicevox Nemo EngineでもVoicevox Engineでも同じ結果が選られるように、 - // GET /speakers、GET /speaker_infoの話者名、スタイル名、画像を差し替える。 - await page.route(/\/speakers$/, async (route) => { - const response = await route.fetch(); - const json: Speaker[] = await response - .json() - .then((json: unknown[]) => json.map(SpeakerFromJSON)); - let i = 0; - for (const speaker of json) { - i++; - speaker.name = `Speaker ${i}`; - let j = 0; - for (const style of speaker.styles) { - j++; - style.name = `Style ${i}-${j}`; - } - } - speakers = json; - await route.fulfill({ - status: 200, - headers: { - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - body: JSON.stringify(json.map(SpeakerToJSON)), - }); - }); - await page.route(/\/speaker_info\?/, async (route) => { - if (!speakers) { - // Unreachableのはず - throw new Error("speakers is not initialized"); - } - const url = new URL(route.request().url()); - const speakerUuid = url.searchParams.get("speaker_uuid"); - if (!speakerUuid) { - throw new Error("speaker_uuid is not set"); - } - const response = await route.fetch(); - const json: SpeakerInfo = await response.json().then(SpeakerInfoFromJSON); - const speakerIndex = speakers.findIndex( - (speaker) => speaker.speakerUuid === speakerUuid, - ); - if (speakerIndex === -1) { - throw new Error(`speaker_uuid=${speakerUuid} is not found`); - } - const image = speakerImages[speakerIndex % speakerImages.length]; - json.portrait = image.portrait; - for (const style of json.styleInfos) { - style.icon = image.icon; - if ("portrait" in style) { - delete style.portrait; - } - } - await route.fulfill({ - status: 200, - headers: { - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - body: JSON.stringify(SpeakerInfoToJSON(json)), - }); - }); -}); test.beforeEach(gotoHome); test("メイン画面の表示", async ({ page }) => { diff --git "a/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\202\275\343\203\263\343\202\260\347\224\273\351\235\242-browser-win32.png" "b/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\202\275\343\203\263\343\202\260\347\224\273\351\235\242-browser-win32.png" index 32da40c0dd535e9c681f9e61a7cedd13bf3eeaa7..c5e5a5971debb631e93b2bd0edf41f683a5b3a9e 100644 GIT binary patch literal 41222 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%0ACd%`joY>4%@ca1vBBdKzT5`yiDxCpO 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 JGC9-V{{cZa6hi<2 literal 31607 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

H_7nbEMhv%0#vw>Lyc^QvGW`mW7l9eirRGdP%- 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 Ti?Sf?L=Y7PP5C@I%lrQW!oLTv diff --git "a/tests/e2e/browser/\350\276\236\346\233\270\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts" "b/tests/e2e/browser/\350\276\236\346\233\270\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts" index 9d8df8039b..bfd2e9b0db 100644 --- "a/tests/e2e/browser/\350\276\236\346\233\270\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts" +++ "b/tests/e2e/browser/\350\276\236\346\233\270\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts" @@ -50,17 +50,12 @@ async function validateInputTag( test("「設定」→「読み方&アクセント辞書」で「読み方&アクセント辞書」ページが表示される", async ({ page, }) => { - test.skip(!process.env.CI, "環境変数CIが未設定のためスキップします"); await navigateToMain(page); - // テスト用にランダムな文字列を生成 - const randomString = Math.random().toString(36).slice(-8); - const zenkakuRandomString = randomString.replace(/[\u0021-\u007e]/g, (s) => { - return String.fromCharCode(s.charCodeAt(0) + 0xfee0); - }); + const targetString = "あいうえお"; // 文字列を入力して読み方を記憶する - const yomi = await getYomi(page, randomString); + const yomi = await getYomi(page, targetString); // 読み方の設定画面を開く await openDictDialog(page); @@ -74,9 +69,9 @@ test("「設定」→「読み方&アクセント辞書」で「読み方& await wordInputTag.evaluate((e: HTMLInputElement, rs: string) => { e.value = rs; e.dispatchEvent(new Event("input")); - }, randomString); + }, targetString); await page.waitForTimeout(100); - await validateInputTag(page, wordInputTag, zenkakuRandomString); + await validateInputTag(page, wordInputTag, targetString); const yomiInputTag = page .locator(".word-editor .row") @@ -101,21 +96,14 @@ test("「設定」→「読み方&アクセント辞書」で「読み方& // 辞書が登録されているかどうかを確認 await page.getByRole("button").filter({ hasText: "add" }).click(); await page.waitForTimeout(100); - const yomi2 = await getYomi(page, randomString); + const yomi2 = await getYomi(page, targetString); expect(yomi2).toBe("テスト"); - // もう一度設定を開き辞書からabsを削除 + // もう一度設定を開き辞書から削除 await openDictDialog(page); - await page - .getByRole("listitem") - .filter({ hasText: zenkakuRandomString }) - .click(); - await page.waitForTimeout(100); - await page - .getByRole("listitem") - .filter({ hasText: zenkakuRandomString }) - .getByText("delete") - .click(); + const wordItem = page.getByRole("listitem").filter({ hasText: targetString }); + await wordItem.hover(); + await wordItem.getByText("delete").click(); await page.waitForTimeout(100); await getNewestQuasarDialog(page) .getByRole("button") @@ -134,6 +122,6 @@ test("「設定」→「読み方&アクセント辞書」で「読み方& // (=最初の読み方と同じになっていることを確認) await page.getByRole("button").filter({ hasText: "add" }).click(); await page.waitForTimeout(100); - const yomi3 = await getYomi(page, randomString); + const yomi3 = await getYomi(page, targetString); expect(yomi3).toBe(yomi); }); diff --git "a/tests/e2e/browser/\351\237\263\345\243\260\350\251\263\347\264\260.spec.ts" "b/tests/e2e/browser/\351\237\263\345\243\260\350\251\263\347\264\260.spec.ts" index dc8533309e..57b873104b 100644 --- "a/tests/e2e/browser/\351\237\263\345\243\260\350\251\263\347\264\260.spec.ts" +++ "b/tests/e2e/browser/\351\237\263\345\243\260\350\251\263\347\264\260.spec.ts" @@ -14,7 +14,7 @@ test("単体アクセント句の読み変更", async ({ page }) => { const textField = page.getByRole("textbox", { name: "1行目" }); await textField.click(); - await textField.fill("1234"); + await textField.fill("あれもこれもそれもどれも"); await textField.press("Enter"); const inputs = Array.from({ length: 4 }, (_, i) => @@ -22,32 +22,32 @@ test("単体アクセント句の読み変更", async ({ page }) => { ); // 読点を追加 - await page.getByText("セ", { exact: true }).click(); - await inputs[0].fill("セン、"); + await page.getByText("ア", { exact: true }).click(); + await inputs[0].fill("アレモ、"); await inputs[0].press("Enter"); await page.waitForTimeout(100); - await expect(page.getByText("セン、")).toBeVisible(); + await expect(page.getByText("アレモ、")).toBeVisible(); // 「,」が読点に変換される - await page.getByText("ヒャ", { exact: true }).click(); - await inputs[1].fill("ニヒャク,"); + await page.getByText("コ", { exact: true }).click(); + await inputs[1].fill("コレモ,"); await inputs[1].press("Enter"); await page.waitForTimeout(100); - await expect(page.getByText("ニヒャク、")).toBeVisible(); + await expect(page.getByText("コレモ、")).toBeVisible(); // 連続する読点を追加すると1つに集約される - await page.getByText("ジュ", { exact: true }).click(); - await inputs[2].fill("サンジュウ,、,、"); + await page.getByText("ソ", { exact: true }).click(); + await inputs[2].fill("ソレモ,、,、"); await inputs[2].press("Enter"); await page.waitForTimeout(100); - await expect(page.getByText("サンジュウ、")).toBeVisible(); + await expect(page.getByText("ソレモ、")).toBeVisible(); // 最後のアクセント区間に読点をつけても無視される - await page.getByText("ヨ", { exact: true }).click(); - await inputs[3].fill("ヨン,、,、"); + await page.getByText("ド", { exact: true }).click(); + await inputs[3].fill("ドレモ,、,、"); await inputs[3].press("Enter"); await page.waitForTimeout(100); - await expect(page.getByText("ヨン、")).not.toBeVisible(); + await expect(page.getByText("ドレモ、")).not.toBeVisible(); }); test("詳細調整欄のコンテキストメニュー", async ({ page }) => { @@ -56,9 +56,11 @@ test("詳細調整欄のコンテキストメニュー", async ({ page }) => { // 削除 await page.getByRole("textbox", { name: "1行目" }).click(); - await page.getByRole("textbox", { name: "1行目" }).fill("1234"); + await page + .getByRole("textbox", { name: "1行目" }) + .fill("あれもこれもそれもどれも"); await page.getByRole("textbox", { name: "1行目" }).press("Enter"); - await page.getByText("サンジュウ").click({ + await page.getByText("ソレモ").click({ button: "right", }); await page @@ -66,7 +68,7 @@ test("詳細調整欄のコンテキストメニュー", async ({ page }) => { .filter({ has: page.getByText("削除") }) .click(); await page.waitForTimeout(100); - await expect(page.getByText("サンジュウ")).not.toBeVisible(); - await expect(page.getByText("ニヒャク")).toBeVisible(); - await expect(page.getByText("ヨン")).toBeVisible(); + await expect(page.getByText("ソレモ")).not.toBeVisible(); + await expect(page.getByText("コレモ")).toBeVisible(); + await expect(page.getByText("ドレモ")).toBeVisible(); }); diff --git a/tests/env/.env.test-e2e b/tests/env/.env.test-electron similarity index 84% rename from tests/env/.env.test-e2e rename to tests/env/.env.test-electron index 0cf137dae0..bb5b8b782e 100644 --- a/tests/env/.env.test-e2e +++ b/tests/env/.env.test-electron @@ -1,4 +1,4 @@ -# CI環境でのe2eテスト用の.envファイル。CI時に値が上書きされる。 +# CI環境でのelectronテスト用の.envファイル。CI時に値が上書きされる。 VITE_APP_NAME=voicevox VITE_DEFAULT_ENGINE_INFOS=`[ diff --git a/tests/unit/backend/common/__snapshots__/configManager.spec.ts.snap b/tests/unit/backend/common/__snapshots__/configManager.spec.ts.snap index 27c1b4d8f6..4da839ee6a 100644 --- a/tests/unit/backend/common/__snapshots__/configManager.spec.ts.snap +++ b/tests/unit/backend/common/__snapshots__/configManager.spec.ts.snap @@ -19,7 +19,7 @@ exports[`0.13.0からマイグレーションできる 1`] = ` "enablePreset": false, "enableRubyNotation": false, "engineSettings": { - "074fc39e-678b-4c13-8916-ffca8d505d1d": { + "00000000-0000-0000-0000-000000000000": { "outputSamplingRate": "engineDefault", "useGpu": false, }, From 15c3d367d56691d4756f690fbbc3d36dc330c679 Mon Sep 17 00:00:00 2001 From: "Nanashi." Date: Sun, 5 Jan 2025 14:59:49 +0900 Subject: [PATCH 21/23] =?UTF-8?q?Change:=20Merge=20Queue=E3=81=AECI?= =?UTF-8?q?=E3=82=921=E5=9B=9E=E3=81=A0=E3=81=91=E8=B5=B0=E3=82=89?= =?UTF-8?q?=E3=81=9B=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B?= =?UTF-8?q?=20(#2475)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b06a4b0dd0..5251fb0178 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,8 +5,6 @@ on: pull_request: branches: - "**" - merge_group: - types: [checks_requested] workflow_dispatch: env: From df4ad342c0f62eadeae1819eaf55c249307626aa Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Sun, 5 Jan 2025 15:15:31 +0900 Subject: [PATCH 22/23] =?UTF-8?q?fix:=20=E3=83=89=E3=83=A9=E3=83=83?= =?UTF-8?q?=E3=82=B0=E3=82=A2=E3=83=B3=E3=83=89=E3=83=89=E3=83=AD=E3=83=83?= =?UTF-8?q?=E3=83=97=E3=81=A7=E3=83=97=E3=83=AD=E3=82=B8=E3=82=A7=E3=82=AF?= =?UTF-8?q?=E3=83=88=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=84=E3=83=86?= =?UTF-8?q?=E3=82=AD=E3=82=B9=E3=83=88=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=81=8C=E8=AA=AD=E3=81=BF=E8=BE=BC=E3=82=81=E3=81=AA=E3=81=8B?= =?UTF-8?q?=E3=81=A3=E3=81=9F=E3=81=AE=E3=82=92=E7=9B=B4=E3=81=99=20(#2433?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/browser/sandbox.ts | 3 +++ src/backend/electron/preload.ts | 7 ++++- src/components/App.vue | 1 + src/components/Menu/MenuBar/MenuBar.vue | 3 ++- src/components/Talk/TalkEditor.vue | 19 ++++++++++++-- src/components/Talk/ToolBar.vue | 2 +- src/components/Talk/menuBarData.ts | 2 +- src/plugins/ipcMessageReceiverPlugin.ts | 7 +++-- src/store/audio.ts | 34 +++++++++++++++++-------- src/store/project.ts | 34 +++++++++++++++---------- src/store/type.ts | 14 ++++++++-- src/type/ipc.ts | 2 +- src/type/preload.ts | 1 + 13 files changed, 93 insertions(+), 36 deletions(-) diff --git a/src/backend/browser/sandbox.ts b/src/backend/browser/sandbox.ts index de3279fb63..f1ef8025ab 100644 --- a/src/backend/browser/sandbox.ts +++ b/src/backend/browser/sandbox.ts @@ -280,4 +280,7 @@ export const api: Sandbox = { reloadApp(/* obj: { isMultiEngineOffMode: boolean } */) { throw new Error(`Not supported on Browser version: reloadApp`); }, + getPathForFile(/* file: File */) { + throw new Error(`Not supported on Browser version: getPathForFile`); + }, }; diff --git a/src/backend/electron/preload.ts b/src/backend/electron/preload.ts index 878c8f6448..23a3370a4f 100644 --- a/src/backend/electron/preload.ts +++ b/src/backend/electron/preload.ts @@ -1,4 +1,4 @@ -import { contextBridge, ipcRenderer } from "electron"; +import { contextBridge, ipcRenderer, webUtils } from "electron"; import type { IpcRendererInvoke } from "./ipc"; import { ConfigType, @@ -228,6 +228,11 @@ const api: Sandbox = { reloadApp: async ({ isMultiEngineOffMode }) => { await ipcRendererInvokeProxy.RELOAD_APP({ isMultiEngineOffMode }); }, + + /** webUtils.getPathForFileを呼ぶ */ + getPathForFile: (file) => { + return webUtils.getPathForFile(file); + }, }; contextBridge.exposeInMainWorld(SandboxKey, api); diff --git a/src/components/App.vue b/src/components/App.vue index f49b89c372..51cdbfea1b 100644 --- a/src/components/App.vue +++ b/src/components/App.vue @@ -165,6 +165,7 @@ onMounted(async () => { // プロジェクトファイルが指定されていればロード if (typeof projectFilePath === "string" && projectFilePath !== "") { isProjectFileLoaded.value = await store.actions.LOAD_PROJECT_FILE({ + type: "path", filePath: projectFilePath, }); } else { diff --git a/src/components/Menu/MenuBar/MenuBar.vue b/src/components/Menu/MenuBar/MenuBar.vue index a94d5d6678..5df82f18d2 100644 --- a/src/components/Menu/MenuBar/MenuBar.vue +++ b/src/components/Menu/MenuBar/MenuBar.vue @@ -157,7 +157,7 @@ const saveProjectAs = async () => { const importProject = () => { if (!uiLocked.value) { - void store.actions.LOAD_PROJECT_FILE({}); + void store.actions.LOAD_PROJECT_FILE({ type: "dialog" }); } }; @@ -198,6 +198,7 @@ const updateRecentProjects = async () => { label: projectFilePath, onClick: () => { void store.actions.LOAD_PROJECT_FILE({ + type: "path", filePath: projectFilePath, }); }, diff --git a/src/components/Talk/TalkEditor.vue b/src/components/Talk/TalkEditor.vue index ecf68b6041..dda58caba7 100644 --- a/src/components/Talk/TalkEditor.vue +++ b/src/components/Talk/TalkEditor.vue @@ -143,6 +143,7 @@ import { actionPostfixSelectNthCharacter, HotkeyActionNameType, } from "@/domain/hotkeyAction"; +import { isElectron } from "@/helpers/platform"; const props = defineProps<{ isEnginesReady: boolean; @@ -574,13 +575,27 @@ const dragEventCounter = ref(0); const loadDraggedFile = (event: { dataTransfer: DataTransfer | null }) => { if (!event.dataTransfer || event.dataTransfer.files.length === 0) return; const file = event.dataTransfer.files[0]; + + // electronの場合のみファイルパスを取得できる + const filePath = isElectron ? window.backend.getPathForFile(file) : undefined; + switch (path.extname(file.name)) { case ".txt": - void store.actions.COMMAND_IMPORT_FROM_FILE({ filePath: file.path }); + if (filePath) { + void store.actions.COMMAND_IMPORT_FROM_FILE({ type: "path", filePath }); + } else { + void store.actions.COMMAND_IMPORT_FROM_FILE({ type: "file", file }); + } break; + case ".vvproj": - void store.actions.LOAD_PROJECT_FILE({ filePath: file.path }); + if (filePath) { + void store.actions.LOAD_PROJECT_FILE({ type: "path", filePath }); + } else { + void store.actions.LOAD_PROJECT_FILE({ type: "file", file }); + } break; + default: void store.actions.SHOW_ALERT_DIALOG({ title: "対応していないファイルです", diff --git a/src/components/Talk/ToolBar.vue b/src/components/Talk/ToolBar.vue index 5bbb6e1e2c..12b1bbbc86 100644 --- a/src/components/Talk/ToolBar.vue +++ b/src/components/Talk/ToolBar.vue @@ -145,7 +145,7 @@ const saveProject = async () => { await store.actions.SAVE_PROJECT_FILE({ overwrite: true }); }; const importTextFile = () => { - void store.actions.COMMAND_IMPORT_FROM_FILE({}); + void store.actions.COMMAND_IMPORT_FROM_FILE({ type: "dialog" }); }; const usableButtons: Record< diff --git a/src/components/Talk/menuBarData.ts b/src/components/Talk/menuBarData.ts index 5a9b73c2ea..cea0fd3a8c 100644 --- a/src/components/Talk/menuBarData.ts +++ b/src/components/Talk/menuBarData.ts @@ -46,7 +46,7 @@ export const useMenuBarData = () => { type: "button", label: "テキスト読み込み", onClick: () => { - void store.actions.COMMAND_IMPORT_FROM_FILE({}); + void store.actions.COMMAND_IMPORT_FROM_FILE({ type: "dialog" }); }, disableWhenUiLocked: true, }, diff --git a/src/plugins/ipcMessageReceiverPlugin.ts b/src/plugins/ipcMessageReceiverPlugin.ts index 1ae8393ca6..78313ee5f3 100644 --- a/src/plugins/ipcMessageReceiverPlugin.ts +++ b/src/plugins/ipcMessageReceiverPlugin.ts @@ -9,8 +9,11 @@ export const ipcMessageReceiver: Plugin = { options: { store: Store }, ) => { window.backend.onReceivedIPCMsg({ - LOAD_PROJECT_FILE: (_, { filePath } = {}) => - void options.store.actions.LOAD_PROJECT_FILE({ filePath }), + LOAD_PROJECT_FILE: (_, { filePath }) => + void options.store.actions.LOAD_PROJECT_FILE({ + type: "path", + filePath, + }), DETECT_MAXIMIZED: () => options.store.actions.DETECT_MAXIMIZED(), diff --git a/src/store/audio.ts b/src/store/audio.ts index d433b55ef6..371b0616f7 100644 --- a/src/store/audio.ts +++ b/src/store/audio.ts @@ -2886,24 +2886,36 @@ export const audioCommandStore = transformCommandStore( prevAudioKey: undefined, }); }, + /** + * セリフテキストファイルを読み込む。 + * ファイル選択ダイアログを表示するか、ファイルパス指定するか、Fileインスタンスを渡すか選べる。 + */ action: createUILockAction( - async ( - { state, mutations, actions, getters }, - { filePath }: { filePath?: string }, - ) => { - if (!filePath) { + async ({ state, mutations, actions, getters }, payload) => { + let filePath: undefined | string; + if (payload.type == "dialog") { filePath = await window.backend.showImportFileDialog({ title: "セリフ読み込み", }); if (!filePath) return; + } else if (payload.type == "path") { + filePath = payload.filePath; } - let body = new TextDecoder("utf-8").decode( - await window.backend.readFile({ filePath }).then(getValueOrThrow), - ); + + let buf: ArrayBuffer; + if (filePath != undefined) { + buf = await window.backend + .readFile({ filePath }) + .then(getValueOrThrow); + } else { + if (payload.type != "file") + throw new UnreachableError("payload.type != 'file'"); + buf = await payload.file.arrayBuffer(); + } + + let body = new TextDecoder("utf-8").decode(buf); if (body.includes("\ufffd")) { - body = new TextDecoder("shift-jis").decode( - await window.backend.readFile({ filePath }).then(getValueOrThrow), - ); + body = new TextDecoder("shift-jis").decode(buf); } const audioItems: AudioItem[] = []; let baseAudioItem: AudioItem | undefined = undefined; diff --git a/src/store/project.ts b/src/store/project.ts index 48da368714..3e5a0862d4 100755 --- a/src/store/project.ts +++ b/src/store/project.ts @@ -21,7 +21,7 @@ import { DEFAULT_TPQN, } from "@/sing/domain"; import { EditorType } from "@/type/preload"; -import { IsEqual } from "@/type/utility"; +import { IsEqual, UnreachableError } from "@/type/utility"; import { showAlertDialog, showMessageDialog, @@ -166,15 +166,13 @@ export const projectStore = createPartialStore({ LOAD_PROJECT_FILE: { /** * プロジェクトファイルを読み込む。読み込めたかの成否が返る。 + * ファイル選択ダイアログを表示するか、ファイルパス指定するか、Fileインスタンスを渡すか選べる。 * エラー発生時はダイアログが表示される。 */ action: createUILockAction( - async ( - { actions, mutations, getters }, - { filePath }: { filePath?: string }, - ) => { - if (!filePath) { - // Select and load a project File. + async ({ actions, mutations, getters }, payload) => { + let filePath: undefined | string; + if (payload.type == "dialog") { const ret = await window.backend.showProjectLoadDialog({ title: "プロジェクトファイルの選択", }); @@ -182,17 +180,25 @@ export const projectStore = createPartialStore({ return false; } filePath = ret[0]; + } else if (payload.type == "path") { + filePath = payload.filePath; } - let buf: ArrayBuffer; try { - buf = await window.backend - .readFile({ filePath }) - .then(getValueOrThrow); + let buf: ArrayBuffer; + if (filePath != undefined) { + buf = await window.backend + .readFile({ filePath }) + .then(getValueOrThrow); - await actions.APPEND_RECENTLY_USED_PROJECT({ - filePath, - }); + await actions.APPEND_RECENTLY_USED_PROJECT({ + filePath, + }); + } else { + if (payload.type != "file") + throw new UnreachableError("payload.type != 'file'"); + buf = await payload.file.arrayBuffer(); + } const text = new TextDecoder("utf-8").decode(buf).trim(); const parsedProjectData = await actions.PARSE_PROJECT_FILE({ diff --git a/src/store/type.ts b/src/store/type.ts index 1e83ab6a0b..a57e243881 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -690,7 +690,12 @@ export type AudioCommandStoreTypes = { mutation: { audioKeyItemPairs: { audioItem: AudioItem; audioKey: AudioKey }[]; }; - action(payload: { filePath?: string }): void; + action( + payload: + | { type: "dialog" } + | { type: "path"; filePath: string } + | { type: "file"; file: File }, + ): void; }; COMMAND_PUT_TEXTS: { @@ -1837,7 +1842,12 @@ export type ProjectStoreTypes = { }; LOAD_PROJECT_FILE: { - action(payload: { filePath?: string }): boolean; + action( + payload: + | { type: "dialog" } + | { type: "path"; filePath: string } + | { type: "file"; file: File }, + ): boolean; }; SAVE_PROJECT_FILE: { diff --git a/src/type/ipc.ts b/src/type/ipc.ts index 3414f186fd..8c35a089f5 100644 --- a/src/type/ipc.ts +++ b/src/type/ipc.ts @@ -241,7 +241,7 @@ export type IpcIHData = { */ export type IpcSOData = { LOAD_PROJECT_FILE: { - args: [obj: { filePath?: string }]; + args: [obj: { filePath: string }]; return: void; }; diff --git a/src/type/preload.ts b/src/type/preload.ts index f916bc7b29..a144d2c64d 100644 --- a/src/type/preload.ts +++ b/src/type/preload.ts @@ -141,6 +141,7 @@ export interface Sandbox { uninstallVvppEngine(engineId: EngineId): Promise; validateEngineDir(engineDir: string): Promise; reloadApp(obj: { isMultiEngineOffMode?: boolean }): Promise; + getPathForFile(file: File): string; } export type AppInfos = { From 10ce8dec6f7c9427c6519f241f926189997af13d Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Wed, 8 Jan 2025 01:21:58 +0900 Subject: [PATCH 23/23] =?UTF-8?q?refactor:=20vvppManager.extractVvpp?= =?UTF-8?q?=E3=81=AE=E5=87=A6=E7=90=86=E3=82=92=E9=96=A2=E6=95=B0=E5=88=87?= =?UTF-8?q?=E3=82=8A=E5=87=BA=E3=81=97=20(#2478)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/electron/manager/vvppManager.ts | 241 +++++++++++--------- 1 file changed, 135 insertions(+), 106 deletions(-) diff --git a/src/backend/electron/manager/vvppManager.ts b/src/backend/electron/manager/vvppManager.ts index 9484a8476e..adaac308bc 100644 --- a/src/backend/electron/manager/vvppManager.ts +++ b/src/backend/electron/manager/vvppManager.ts @@ -33,6 +33,136 @@ export const isVvppFile = (filePath: string) => { const lockKey = "lock-key-for-vvpp-manager"; +/** VVPPファイルが分割されている場合、それらのファイルを取得する */ +async function getArchiveFileParts( + vvppLikeFilePath: string, +): Promise { + let archiveFileParts: string[]; + // 名前.数値.vvpppの場合は分割されているとみなして連結する + if (vvppLikeFilePath.match(/\.[0-9]+\.vvppp$/)) { + log.log("vvpp is split, finding other parts..."); + const vvpppPathGlob = vvppLikeFilePath + .replace(/\.[0-9]+\.vvppp$/, ".*.vvppp") + .replace(/\\/g, "/"); // node-globはバックスラッシュを使えないので、スラッシュに置換する + const filePaths: string[] = []; + for (const p of await glob(vvpppPathGlob)) { + if (!p.match(/\.[0-9]+\.vvppp$/)) { + continue; + } + log.log(`found ${p}`); + filePaths.push(p); + } + filePaths.sort((a, b) => { + const aMatch = a.match(/\.([0-9]+)\.vvppp$/); + const bMatch = b.match(/\.([0-9]+)\.vvppp$/); + if (aMatch == null || bMatch == null) { + throw new Error(`match is null: a=${a}, b=${b}`); + } + return parseInt(aMatch[1]) - parseInt(bMatch[1]); + }); + archiveFileParts = filePaths; + } else { + log.log("Not a split file"); + archiveFileParts = [vvppLikeFilePath]; + } + return archiveFileParts; +} + +/** 分割されているVVPPファイルを連結して返す */ +async function concatenateVvppFiles( + format: "zip" | "7z", + archiveFileParts: string[], +) { + // -siオプションでの7z解凍はサポートされていないため、 + // ファイルを連結した一次ファイルを作成し、それを7zで解凍する。 + log.log(`Concatenating ${archiveFileParts.length} files...`); + const tmpConcatenatedFile = path.join( + app.getPath("temp"), + `vvpp-${new Date().getTime()}.${format}`, + ); + log.log("Temporary file:", tmpConcatenatedFile); + await new Promise((resolve, reject) => { + if (!tmpConcatenatedFile) throw new Error("tmpFile is undefined"); + const inputStreams = archiveFileParts.map((f) => fs.createReadStream(f)); + const outputStream = fs.createWriteStream(tmpConcatenatedFile); + new MultiStream(inputStreams) + .pipe(outputStream) + .on("close", () => { + outputStream.close(); + resolve(); + }) + .on("error", reject); + }); + log.log("Concatenated"); + return tmpConcatenatedFile; +} + +/** 7zでファイルを解凍する */ +async function unarchive( + payload: { + archiveFile: string; + outputDir: string; + format: "zip" | "7z"; + }, + callbacks?: { onProgress?: ProgressCallback }, +) { + const { archiveFile, outputDir, format } = payload; + + const args = [ + "x", + "-o" + outputDir, + archiveFile, + "-t" + format, + "-bsp1", // 進捗出力 + ]; + + let sevenZipPath = import.meta.env.VITE_7Z_BIN_NAME; + if (!sevenZipPath) { + throw new Error("7z path is not defined"); + } + if (import.meta.env.PROD) { + sevenZipPath = path.join(path.dirname(app.getPath("exe")), sevenZipPath); + } + log.log("Spawning 7z:", sevenZipPath, args.join(" ")); + await new Promise((resolve, reject) => { + const child = spawn(sevenZipPath, args, { + stdio: ["pipe", "pipe", "pipe"], + }); + + child.stdout?.on("data", (data: Buffer) => { + const output = data.toString("utf-8"); + log.info(`7z STDOUT: ${output}`); + + // 進捗を取得 + // NOTE: ` 75% 106 - pyopenjtalk\open_jtalk_dic_utf_8-1.11\sys.dic` のような出力が来る + // TODO: 出力が変わるかもしれないのでテストが必要 + const progressMatch = output.match( + / *(?\d+)% ?(?\d+)? ?(?.*)/, + ); + if (progressMatch?.groups?.percent) { + callbacks?.onProgress?.({ + progress: parseInt(progressMatch.groups.percent), + }); + } + }); + + child.stderr?.on("data", (data: Buffer) => { + log.error(`7z STDERR: ${data.toString("utf-8")}`); + }); + + child.on("exit", (code) => { + if (code === 0) { + callbacks?.onProgress?.({ progress: 100 }); + resolve(); + } else { + reject(new Error(`7z exited with code ${code}`)); + } + }); + // FIXME: rejectが2回呼ばれることがある + child.on("error", reject); + }); +} + // # 軽い概要 // // フォルダ名:"エンジン名+UUID" @@ -117,34 +247,7 @@ export class VvppManager { const nonce = new Date().getTime().toString(); const outputDir = path.join(this.vvppEngineDir, ".tmp", nonce); - let archiveFileParts: string[]; - // 名前.数値.vvpppの場合は分割されているとみなして連結する - if (vvppLikeFilePath.match(/\.[0-9]+\.vvppp$/)) { - log.log("vvpp is split, finding other parts..."); - const vvpppPathGlob = vvppLikeFilePath - .replace(/\.[0-9]+\.vvppp$/, ".*.vvppp") - .replace(/\\/g, "/"); // node-globはバックスラッシュを使えないので、スラッシュに置換する - const filePaths: string[] = []; - for (const p of await glob(vvpppPathGlob)) { - if (!p.match(/\.[0-9]+\.vvppp$/)) { - continue; - } - log.log(`found ${p}`); - filePaths.push(p); - } - filePaths.sort((a, b) => { - const aMatch = a.match(/\.([0-9]+)\.vvppp$/); - const bMatch = b.match(/\.([0-9]+)\.vvppp$/); - if (aMatch == null || bMatch == null) { - throw new Error(`match is null: a=${a}, b=${b}`); - } - return parseInt(aMatch[1]) - parseInt(bMatch[1]); - }); - archiveFileParts = filePaths; - } else { - log.log("Not a split file"); - archiveFileParts = [vvppLikeFilePath]; - } + const archiveFileParts = await getArchiveFileParts(vvppLikeFilePath); const format = await this.detectFileFormat(archiveFileParts[0]); if (!format) { @@ -157,91 +260,17 @@ export class VvppManager { let archiveFile: string; try { if (archiveFileParts.length > 1) { - // -siオプションでの7z解凍はサポートされていないため、 - // ファイルを連結した一次ファイルを作成し、それを7zで解凍する。 - log.log(`Concatenating ${archiveFileParts.length} files...`); - tmpConcatenatedFile = path.join( - app.getPath("temp"), - `vvpp-${new Date().getTime()}.${format}`, + tmpConcatenatedFile = await concatenateVvppFiles( + format, + archiveFileParts, ); - log.log("Temporary file:", tmpConcatenatedFile); archiveFile = tmpConcatenatedFile; - await new Promise((resolve, reject) => { - if (!tmpConcatenatedFile) throw new Error("tmpFile is undefined"); - const inputStreams = archiveFileParts.map((f) => - fs.createReadStream(f), - ); - const outputStream = fs.createWriteStream(tmpConcatenatedFile); - new MultiStream(inputStreams) - .pipe(outputStream) - .on("close", () => { - outputStream.close(); - resolve(); - }) - .on("error", reject); - }); - log.log("Concatenated"); } else { archiveFile = archiveFileParts[0]; log.log("Single file, not concatenating"); } - const args = [ - "x", - "-o" + outputDir, - archiveFile, - "-t" + format, - "-bsp1", // 進捗出力 - ]; - - let sevenZipPath = import.meta.env.VITE_7Z_BIN_NAME; - if (!sevenZipPath) { - throw new Error("7z path is not defined"); - } - if (import.meta.env.PROD) { - sevenZipPath = path.join( - path.dirname(app.getPath("exe")), - sevenZipPath, - ); - } - log.log("Spawning 7z:", sevenZipPath, args.join(" ")); - await new Promise((resolve, reject) => { - const child = spawn(sevenZipPath, args, { - stdio: ["pipe", "pipe", "pipe"], - }); - - child.stdout?.on("data", (data: Buffer) => { - const output = data.toString("utf-8"); - log.info(`7z STDOUT: ${output}`); - - // 進捗を取得 - // NOTE: ` 75% 106 - pyopenjtalk\open_jtalk_dic_utf_8-1.11\sys.dic` のような出力が来る - // TODO: 出力が変わるかもしれないのでテストが必要 - const progressMatch = output.match( - / *(?\d+)% ?(?\d+)? ?(?.*)/, - ); - if (progressMatch?.groups?.percent) { - callbacks?.onProgress?.({ - progress: parseInt(progressMatch.groups.percent), - }); - } - }); - - child.stderr?.on("data", (data: Buffer) => { - log.error(`7z STDERR: ${data.toString("utf-8")}`); - }); - - child.on("exit", (code) => { - if (code === 0) { - callbacks?.onProgress?.({ progress: 100 }); - resolve(); - } else { - reject(new Error(`7z exited with code ${code}`)); - } - }); - // FIXME: rejectが2回呼ばれることがある - child.on("error", reject); - }); + await unarchive({ archiveFile, outputDir, format }, callbacks); } finally { if (tmpConcatenatedFile) { log.log("Removing temporary file", tmpConcatenatedFile);