Skip to content

Commit

Permalink
VVPPをデフォルトエンジンに指定可能にし、未インストール時にインストールするか聞くようにする (VOICEVOX#2270)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hiroshiba authored Dec 5, 2024
1 parent 8990841 commit e829c02
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 37 deletions.
3 changes: 3 additions & 0 deletions src/backend/browser/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
98 changes: 98 additions & 0 deletions src/backend/electron/engineAndVvppController.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import path from "path";
import fs from "fs";
import log from "electron-log/main";
import { BrowserWindow, dialog } from "electron";

Expand All @@ -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周りの処理の流れを制御するクラス。
Expand Down Expand Up @@ -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");
Expand Down
24 changes: 24 additions & 0 deletions src/backend/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
51 changes: 31 additions & 20 deletions src/backend/electron/manager/engineInfoManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ 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;

/** 代替ポート情報 */
public altPortInfos: AltPortInfos = {};

private envEngineInfos = loadEnvEngineInfos();

constructor(payload: { defaultEngineDir: string; vvppEngineDir: string }) {
this.defaultEngineDir = payload.defaultEngineDir;
this.vvppEngineDir = payload.vvppEngineDir;
Expand Down Expand Up @@ -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;
});
}

/**
Expand Down Expand Up @@ -178,6 +181,14 @@ export class EngineInfoManager {
return engineInfo;
}

/**
* 指定したエンジンの情報が存在するかどうかを判定する。
*/
hasEngineInfo(engineId: EngineId): boolean {
const engineInfos = this.fetchEngineInfos();
return engineInfos.some((engineInfo) => engineInfo.uuid === engineId);
}

/**
* エンジンのディレクトリを取得する。存在しない場合はエラーを返す。
*/
Expand Down
34 changes: 24 additions & 10 deletions src/domain/defaultEngine/envEngineInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof envEngineInfoSchema>;
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<typeof envEngineInfoSchema>;

/** .envからデフォルトエンジン情報を読み込む */
export function loadEnvEngineInfos(): EnvEngineInfoType[] {
Expand Down
44 changes: 37 additions & 7 deletions src/domain/defaultEngine/latetDefaultEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { z } from "zod";

/** パッケージ情報のスキーマ */
const engineVariantSchema = z.object({
const packageInfoSchema = z.object({
version: z.string(),
packages: z
.object({
Expand All @@ -16,28 +16,29 @@ const engineVariantSchema = z.object({
})
.array(),
});
export type PackageInfo = z.infer<typeof packageInfoSchema>;

/** デフォルトエンジンの最新情報のスキーマ */
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,
}),
}),
});
Expand All @@ -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<typeof latestDefaultEngineInfoSchema>,
): 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}`);
};

0 comments on commit e829c02

Please sign in to comment.