From e829c020c20efe5ac4db1011f1c0f10c8a6d9ebb Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Fri, 6 Dec 2024 02:29:02 +0900 Subject: [PATCH] =?UTF-8?q?VVPP=E3=82=92=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=AB?= =?UTF-8?q?=E6=8C=87=E5=AE=9A=E5=8F=AF=E8=83=BD=E3=81=AB=E3=81=97=E3=80=81?= =?UTF-8?q?=E6=9C=AA=E3=82=A4=E3=83=B3=E3=82=B9=E3=83=88=E3=83=BC=E3=83=AB?= =?UTF-8?q?=E6=99=82=E3=81=AB=E3=82=A4=E3=83=B3=E3=82=B9=E3=83=88=E3=83=BC?= =?UTF-8?q?=E3=83=AB=E3=81=99=E3=82=8B=E3=81=8B=E8=81=9E=E3=81=8F=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B=20(#2270)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/browser/contract.ts | 3 + .../electron/engineAndVvppController.ts | 98 +++++++++++++++++++ src/backend/electron/main.ts | 24 +++++ .../electron/manager/engineInfoManager.ts | 51 ++++++---- src/domain/defaultEngine/envEngineInfo.ts | 34 +++++-- .../defaultEngine/latetDefaultEngine.ts | 44 +++++++-- 6 files changed, 217 insertions(+), 37 deletions(-) diff --git a/src/backend/browser/contract.ts b/src/backend/browser/contract.ts index 0e8c783f4e..a7ea82fab9 100644 --- a/src/backend/browser/contract.ts +++ b/src/backend/browser/contract.ts @@ -2,6 +2,9 @@ import { loadEnvEngineInfos } from "@/domain/defaultEngine/envEngineInfo"; import { type EngineInfo } from "@/type/preload"; const baseEngineInfo = loadEnvEngineInfos()[0]; +if (baseEngineInfo.type != "path") { + throw new Error("default engine type must be path"); +} export const defaultEngine: EngineInfo = (() => { const { protocol, hostname, port, pathname } = new URL(baseEngineInfo.host); diff --git a/src/backend/electron/engineAndVvppController.ts b/src/backend/electron/engineAndVvppController.ts index dd8e9a525e..5e0622c3ef 100644 --- a/src/backend/electron/engineAndVvppController.ts +++ b/src/backend/electron/engineAndVvppController.ts @@ -1,3 +1,5 @@ +import path from "path"; +import fs from "fs"; import log from "electron-log/main"; import { BrowserWindow, dialog } from "electron"; @@ -12,6 +14,13 @@ import { engineSettingSchema, EngineSettingType, } from "@/type/preload"; +import { + PackageInfo, + fetchLatestDefaultEngineInfo, + getSuitablePackageInfo, +} from "@/domain/defaultEngine/latetDefaultEngine"; +import { loadEnvEngineInfos } from "@/domain/defaultEngine/envEngineInfo"; +import { UnreachableError } from "@/type/utility"; /** * エンジンとVVPP周りの処理の流れを制御するクラス。 @@ -131,6 +140,95 @@ export class EngineAndVvppController { } } + /** + * インストール可能なデフォルトエンジンの情報とパッケージの情報を取得する。 + */ + async fetchInsallablePackageInfos(): Promise< + { engineName: string; packageInfo: PackageInfo }[] + > { + // ダウンロード可能なVVPPのうち、未インストールのものを返す + const targetInfos = []; + for (const envEngineInfo of loadEnvEngineInfos()) { + if (envEngineInfo.type != "downloadVvpp") { + continue; + } + + // 最新情報を取得 + const latestUrl = envEngineInfo.latestUrl; + if (latestUrl == undefined) throw new Error("latestUrl is undefined"); + + const latestInfo = await fetchLatestDefaultEngineInfo(latestUrl); + if (latestInfo.formatVersion != 1) { + log.error(`Unsupported format version: ${latestInfo.formatVersion}`); + continue; + } + + // 実行環境に合うパッケージを取得 + const packageInfo = getSuitablePackageInfo(latestInfo); + log.info(`Latest default engine version: ${packageInfo.version}`); + + // インストール済みだった場合はスキップ + // FIXME: より新しいバージョンがあれば更新できるようにする + if (this.engineInfoManager.hasEngineInfo(envEngineInfo.uuid)) { + log.info(`Default engine ${envEngineInfo.uuid} is already installed.`); + continue; + } + + targetInfos.push({ engineName: envEngineInfo.name, packageInfo }); + } + + return targetInfos; + } + + /** VVPPパッケージをダウンロードし、インストールする */ + async downloadAndInstallVvppEngine( + downloadDir: string, + packageInfo: PackageInfo, + ) { + if (packageInfo.packages.length === 0) { + throw new UnreachableError("No packages to download"); + } + + let failed = false; + const downloadedPaths: string[] = []; + try { + // ダウンロード + 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 downloadPath = path.join(downloadDir, name); + await fs.promises.writeFile(downloadPath, Buffer.from(buffer)); // TODO: オンメモリじゃなくする + log.info(`Downloaded ${name} to ${downloadPath}`); + + downloadedPaths.push(downloadPath); + + // TODO: ハッシュチェック + }), + ); + + // インストール + await this.installVvppEngine(downloadedPaths[0]); + } catch (e) { + failed = true; + log.error(`Failed to download and install VVPP engine:`, e); + throw e; + } finally { + // ダウンロードしたファイルを削除 + await Promise.all( + downloadedPaths.map(async (path) => { + log.info(`Delete downloaded file: ${path}`); + await fs.promises.unlink(path); + }), + ); + } + } + /** エンジンの設定を更新し、保存する */ updateEngineSetting(engineId: EngineId, engineSetting: EngineSettingType) { const engineSettings = this.configManager.get("engineSettings"); diff --git a/src/backend/electron/main.ts b/src/backend/electron/main.ts index 22d3c34fb2..bfcef34952 100644 --- a/src/backend/electron/main.ts +++ b/src/backend/electron/main.ts @@ -949,6 +949,30 @@ app.on("ready", async () => { } } + // VVPPがデフォルトエンジンに指定されていたらインストールする + // NOTE: この機能は工事中。参照: https://github.com/VOICEVOX/voicevox/issues/1194 + const packageInfos = + await engineAndVvppController.fetchInsallablePackageInfos(); + for (const { engineName, packageInfo } of packageInfos) { + // インストールするか確認 + const result = dialog.showMessageBoxSync(win, { + type: "info", + title: "デフォルトエンジンのインストール", + message: `${engineName} をインストールしますか?`, + buttons: ["インストール", "キャンセル"], + cancelId: 1, + }); + if (result == 1) { + continue; + } + + // ダウンロードしてインストールする + await engineAndVvppController.downloadAndInstallVvppEngine( + app.getPath("downloads"), + packageInfo, + ); + } + // runEngineAllの前にVVPPを読み込む let filePath: string | undefined; if (process.platform === "darwin") { diff --git a/src/backend/electron/manager/engineInfoManager.ts b/src/backend/electron/manager/engineInfoManager.ts index f9471194f0..e44ce248ef 100644 --- a/src/backend/electron/manager/engineInfoManager.ts +++ b/src/backend/electron/manager/engineInfoManager.ts @@ -18,7 +18,7 @@ import { AltPortInfos } from "@/store/type"; import { loadEnvEngineInfos } from "@/domain/defaultEngine/envEngineInfo"; import { failure, Result, success } from "@/type/result"; -/** エンジンの情報を管理するクラス */ +/** 利用可能なエンジンの情報を管理するクラス */ export class EngineInfoManager { defaultEngineDir: string; vvppEngineDir: string; @@ -26,6 +26,8 @@ export class EngineInfoManager { /** 代替ポート情報 */ public altPortInfos: AltPortInfos = {}; + private envEngineInfos = loadEnvEngineInfos(); + constructor(payload: { defaultEngineDir: string; vvppEngineDir: string }) { this.defaultEngineDir = payload.defaultEngineDir; this.vvppEngineDir = payload.vvppEngineDir; @@ -74,28 +76,29 @@ export class EngineInfoManager { /** * .envにあるエンジンの情報を取得する。 + * ダウンロードが必要なものは除外されている。 */ private fetchEnvEngineInfos(): EngineInfo[] { // TODO: envから直接ではなく、envに書いたengine_manifest.jsonから情報を得るようにする - const engines = loadEnvEngineInfos(); - - return engines.map((engineInfo) => { - const { protocol, hostname, port, pathname } = new URL(engineInfo.host); - return { - ...engineInfo, - protocol, - hostname, - defaultPort: port, - pathname: pathname === "/" ? "" : pathname, - isDefault: true, - type: "path", - executionFilePath: path.resolve(engineInfo.executionFilePath), - path: - engineInfo.path == undefined - ? undefined - : path.resolve(this.defaultEngineDir, engineInfo.path), - } satisfies EngineInfo; - }); + return this.envEngineInfos + .filter((engineInfo) => engineInfo.type != "downloadVvpp") + .map((engineInfo) => { + const { protocol, hostname, port, pathname } = new URL(engineInfo.host); + return { + ...engineInfo, + protocol, + hostname, + defaultPort: port, + pathname: pathname === "/" ? "" : pathname, + isDefault: true, + type: engineInfo.type, + executionFilePath: path.resolve(engineInfo.executionFilePath), + path: + engineInfo.path == undefined + ? undefined + : path.resolve(this.defaultEngineDir, engineInfo.path), + } satisfies EngineInfo; + }); } /** @@ -178,6 +181,14 @@ export class EngineInfoManager { return engineInfo; } + /** + * 指定したエンジンの情報が存在するかどうかを判定する。 + */ + hasEngineInfo(engineId: EngineId): boolean { + const engineInfos = this.fetchEngineInfos(); + return engineInfos.some((engineInfo) => engineInfo.uuid === engineId); + } + /** * エンジンのディレクトリを取得する。存在しない場合はエラーを返す。 */ diff --git a/src/domain/defaultEngine/envEngineInfo.ts b/src/domain/defaultEngine/envEngineInfo.ts index 3003f70c62..f45098c264 100644 --- a/src/domain/defaultEngine/envEngineInfo.ts +++ b/src/domain/defaultEngine/envEngineInfo.ts @@ -7,16 +7,30 @@ import { z } from "zod"; import { engineIdSchema } from "@/type/preload"; /** .envに書くデフォルトエンジン情報のスキーマ */ -export const envEngineInfoSchema = z.object({ - uuid: engineIdSchema, - host: z.string(), - name: z.string(), - executionEnabled: z.boolean(), - executionFilePath: z.string(), - executionArgs: z.array(z.string()), - path: z.string().optional(), -}); -export type EnvEngineInfoType = z.infer; +const envEngineInfoSchema = z + .object({ + uuid: engineIdSchema, + host: z.string(), + name: z.string(), + executionEnabled: z.boolean(), + executionArgs: z.array(z.string()), + }) + .and( + z.union([ + // エンジンをパス指定する場合 + z.object({ + type: z.literal("path").default("path"), + executionFilePath: z.string(), + path: z.string().optional(), + }), + // VVPPダウンロードする場合 + z.object({ + type: z.literal("downloadVvpp"), + latestUrl: z.string(), + }), + ]), + ); +type EnvEngineInfoType = z.infer; /** .envからデフォルトエンジン情報を読み込む */ export function loadEnvEngineInfos(): EnvEngineInfoType[] { diff --git a/src/domain/defaultEngine/latetDefaultEngine.ts b/src/domain/defaultEngine/latetDefaultEngine.ts index 62c95534c8..f07aaaeb87 100644 --- a/src/domain/defaultEngine/latetDefaultEngine.ts +++ b/src/domain/defaultEngine/latetDefaultEngine.ts @@ -5,7 +5,7 @@ import { z } from "zod"; /** パッケージ情報のスキーマ */ -const engineVariantSchema = z.object({ +const packageInfoSchema = z.object({ version: z.string(), packages: z .object({ @@ -16,28 +16,29 @@ const engineVariantSchema = z.object({ }) .array(), }); +export type PackageInfo = z.infer; /** デフォルトエンジンの最新情報のスキーマ */ const latestDefaultEngineInfoSchema = z.object({ formatVersion: z.number(), windows: z.object({ x64: z.object({ - CPU: engineVariantSchema, - "GPU/CPU": engineVariantSchema, + CPU: packageInfoSchema, + "GPU/CPU": packageInfoSchema, }), }), macos: z.object({ x64: z.object({ - CPU: engineVariantSchema, + CPU: packageInfoSchema, }), arm64: z.object({ - CPU: engineVariantSchema, + CPU: packageInfoSchema, }), }), linux: z.object({ x64: z.object({ - CPU: engineVariantSchema, - "GPU/CPU": engineVariantSchema, + CPU: packageInfoSchema, + "GPU/CPU": packageInfoSchema, }), }), }); @@ -47,3 +48,32 @@ export const fetchLatestDefaultEngineInfo = async (url: string) => { const response = await fetch(url); return latestDefaultEngineInfoSchema.parse(await response.json()); }; + +/** + * 実行環境に合うパッケージを取得する。GPU版があればGPU版を返す。 + * TODO: どのデバイス版にするかはユーザーが選べるようにするべき。 + */ +export const getSuitablePackageInfo = ( + updateInfo: z.infer, +): PackageInfo => { + const platform = process.platform; + const arch = process.arch; + + if (platform === "win32") { + if (arch === "x64") { + return updateInfo.windows.x64["GPU/CPU"]; + } + } else if (platform === "darwin") { + if (arch === "x64") { + return updateInfo.macos.x64.CPU; + } else if (arch === "arm64") { + return updateInfo.macos.arm64.CPU; + } + } else if (platform === "linux") { + if (arch === "x64") { + return updateInfo.linux.x64["GPU/CPU"]; + } + } + + throw new Error(`Unsupported platform: ${platform} ${arch}`); +};