Feat: Improve first setup (#319)
* remove whisper model checking when setup * fix landing page step * refactor whisper * refactor whisper options * update workflow * update test-enjoy-app.yml
This commit is contained in:
7
.github/workflows/release-enjoy-app.yml
vendored
7
.github/workflows/release-enjoy-app.yml
vendored
@@ -6,7 +6,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, macos-13-xlarge, windows-latest, ubuntu-latest]
|
||||
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -18,3 +18,8 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
|
||||
run: yarn publish:enjoy
|
||||
- if: matrix.os == 'macos-latest'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
|
||||
PACKAGE_OS_ARCH: arm64
|
||||
run: yarn run publish:enjoy --arch=arm64
|
||||
|
||||
8
.github/workflows/test-enjoy-app.yml
vendored
8
.github/workflows/test-enjoy-app.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
- "enjoy/**/*.js"
|
||||
- "enjoy/**/*.mjs"
|
||||
jobs:
|
||||
test:
|
||||
e2e:
|
||||
timeout-minutes: 60
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
[
|
||||
macos-latest,
|
||||
macos-13,
|
||||
macos-13-xlarge,
|
||||
macos-14,
|
||||
windows-2019,
|
||||
windows-latest,
|
||||
ubuntu-20.04,
|
||||
@@ -38,11 +38,11 @@ jobs:
|
||||
run: |
|
||||
brew update
|
||||
brew install sdl2
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
- if: startsWith(matrix.os, 'ubuntu')
|
||||
name: Run tests with xvfb-run on ubuntu
|
||||
run: |
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:enjoy
|
||||
- if: matrix.os != 'ubuntu-latest'
|
||||
- if: startsWith(matrix.os, 'macos') || startsWith(matrix.os, 'windows')
|
||||
name: Run tests
|
||||
run: yarn test:enjoy
|
||||
- uses: actions/upload-artifact@v4
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -115,6 +115,7 @@ package-lock.json
|
||||
*/playwright-report/
|
||||
*/blob-report/
|
||||
*/playwright/.cache/
|
||||
*/tmp/
|
||||
|
||||
# whisper models
|
||||
ggml-*.bin
|
||||
|
||||
@@ -19,6 +19,7 @@ declare global {
|
||||
}
|
||||
|
||||
let electronApp: ElectronApplication;
|
||||
const resultDir = path.join(process.cwd(), "test-results");
|
||||
|
||||
test.beforeAll(async () => {
|
||||
// find the latest build in the out directory
|
||||
@@ -28,8 +29,6 @@ test.beforeAll(async () => {
|
||||
// set the CI environment variable to true
|
||||
process.env.CI = "e2e";
|
||||
|
||||
const resultDir = path.join(process.cwd(), "test-results");
|
||||
|
||||
fs.ensureDirSync(resultDir);
|
||||
process.env.SETTINGS_PATH = resultDir;
|
||||
process.env.LIBRARY_PATH = resultDir;
|
||||
@@ -40,7 +39,7 @@ test.beforeAll(async () => {
|
||||
});
|
||||
electronApp.on("window", async (page) => {
|
||||
const filename = page.url()?.split("/").pop();
|
||||
console.log(`Window opened: ${filename}`);
|
||||
console.info(`Window opened: ${filename}`);
|
||||
|
||||
// capture errors
|
||||
page.on("pageerror", (error) => {
|
||||
@@ -48,7 +47,7 @@ test.beforeAll(async () => {
|
||||
});
|
||||
// capture console messages
|
||||
page.on("console", (msg) => {
|
||||
console.log(msg.text());
|
||||
console.info(msg.text());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -57,25 +56,26 @@ test.afterAll(async () => {
|
||||
await electronApp.close();
|
||||
});
|
||||
|
||||
let page: Page;
|
||||
|
||||
test("renders the first page", async () => {
|
||||
page = await electronApp.firstWindow();
|
||||
const page = await electronApp.firstWindow();
|
||||
const title = await page.title();
|
||||
expect(title).toBe("Enjoy");
|
||||
});
|
||||
|
||||
test("validate whisper command", async () => {
|
||||
page = await electronApp.firstWindow();
|
||||
const page = await electronApp.firstWindow();
|
||||
const res = await page.evaluate(() => {
|
||||
return window.__ENJOY_APP__.whisper.check();
|
||||
});
|
||||
console.info(res.log);
|
||||
expect(res.success).toBeTruthy();
|
||||
|
||||
const settings = fs.readJsonSync(path.join(resultDir, "settings.json"));
|
||||
expect(settings.whisper.service).toBe("local");
|
||||
});
|
||||
|
||||
test("valid ffmpeg command", async () => {
|
||||
page = await electronApp.firstWindow();
|
||||
const page = await electronApp.firstWindow();
|
||||
const res = await page.evaluate(() => {
|
||||
return window.__ENJOY_APP__.ffmpeg.check();
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"types": "./src/types.d.ts",
|
||||
"scripts": {
|
||||
"predev": "yarn run download",
|
||||
"dev": "rimraf .vite && yarn run download && WEB_API_URL=http://localhost:3000 electron-forge start",
|
||||
"dev": "rimraf .vite && yarn run download && WEB_API_URL=http://localhost:3000 SETTINGS_PATH=./tmp LIBRARY_PATH=./tmp electron-forge start",
|
||||
"start": "rimraf .vite && yarn run download && electron-forge start",
|
||||
"package": "rimraf .vite && yarn run download && electron-forge package",
|
||||
"make": "rimraf .vite && yarn run download && electron-forge make",
|
||||
|
||||
@@ -70,13 +70,8 @@ const whisperConfig = (): WhisperConfigType => {
|
||||
) as WhisperConfigType["service"];
|
||||
|
||||
if (!service) {
|
||||
if (model) {
|
||||
settings.setSync("whisper.service", "local");
|
||||
service = "local";
|
||||
} else {
|
||||
settings.setSync("whisper.service", "azure");
|
||||
service = "azure";
|
||||
}
|
||||
settings.setSync("whisper.service", "local");
|
||||
service = "local";
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -13,8 +13,7 @@ class Whipser {
|
||||
private bundledModelsDir: string;
|
||||
public config: WhisperConfigType;
|
||||
|
||||
constructor(config?: WhisperConfigType) {
|
||||
this.config = config || settings.whisperConfig();
|
||||
constructor() {
|
||||
const customWhisperPath = path.join(
|
||||
settings.libraryPath(),
|
||||
"whisper",
|
||||
@@ -26,26 +25,10 @@ class Whipser {
|
||||
} else {
|
||||
this.binMain = path.join(__dirname, "lib", "whisper", "main");
|
||||
}
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
currentModel() {
|
||||
if (!this.config.availableModels) return;
|
||||
|
||||
let model: WhisperConfigType["availableModels"][0];
|
||||
if (this.config.model) {
|
||||
model = (this.config.availableModels || []).find(
|
||||
(m) => m.name === this.config.model
|
||||
);
|
||||
}
|
||||
if (!model) {
|
||||
model = this.config.availableModels[0];
|
||||
}
|
||||
|
||||
settings.setSync("whisper.model", model.name);
|
||||
return model.savePath;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
initialize() {
|
||||
const models = [];
|
||||
|
||||
const bundledModels = fs.readdirSync(this.bundledModelsDir);
|
||||
@@ -74,44 +57,26 @@ class Whipser {
|
||||
settings.setSync("whisper.availableModels", models);
|
||||
settings.setSync("whisper.modelsPath", dir);
|
||||
this.config = settings.whisperConfig();
|
||||
}
|
||||
|
||||
const command = `"${this.binMain}" --help`;
|
||||
logger.debug(`Checking whisper command: ${command}`);
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(
|
||||
command,
|
||||
{
|
||||
timeout: PROCESS_TIMEOUT,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
logger.error("error", error);
|
||||
}
|
||||
currentModel() {
|
||||
if (!this.config.availableModels) return;
|
||||
|
||||
if (stderr) {
|
||||
logger.debug("stderr", stderr);
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
logger.debug("stdout", stdout);
|
||||
}
|
||||
|
||||
const std = (stdout || stderr).toString()?.trim();
|
||||
if (std.startsWith("usage:")) {
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(
|
||||
error || new Error("Whisper check failed: unknown error").message
|
||||
);
|
||||
}
|
||||
}
|
||||
let model: WhisperConfigType["availableModels"][0];
|
||||
if (this.config.model) {
|
||||
model = (this.config.availableModels || []).find(
|
||||
(m) => m.name === this.config.model
|
||||
);
|
||||
});
|
||||
}
|
||||
if (!model) {
|
||||
model = this.config.availableModels[0];
|
||||
}
|
||||
|
||||
settings.setSync("whisper.model", model.name);
|
||||
return model.savePath;
|
||||
}
|
||||
|
||||
async check() {
|
||||
await this.initialize();
|
||||
|
||||
const model = this.currentModel();
|
||||
logger.debug(`Checking whisper model: ${model}`);
|
||||
|
||||
@@ -262,12 +227,7 @@ class Whipser {
|
||||
|
||||
registerIpcHandlers() {
|
||||
ipcMain.handle("whisper-config", async () => {
|
||||
try {
|
||||
await this.initialize();
|
||||
return Object.assign({}, this.config, { ready: true });
|
||||
} catch (_err) {
|
||||
return Object.assign({}, this.config, { ready: false });
|
||||
}
|
||||
return this.config;
|
||||
});
|
||||
|
||||
ipcMain.handle("whisper-set-model", async (event, model) => {
|
||||
@@ -295,7 +255,7 @@ class Whipser {
|
||||
ipcMain.handle("whisper-set-service", async (event, service) => {
|
||||
if (service === "local") {
|
||||
try {
|
||||
await this.initialize();
|
||||
await this.check();
|
||||
settings.setSync("whisper.service", service);
|
||||
this.config.service = service;
|
||||
return this.config;
|
||||
|
||||
@@ -8,18 +8,12 @@ import {
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
Button,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
ScrollArea,
|
||||
toast,
|
||||
Progress,
|
||||
} from "@renderer/components/ui";
|
||||
import { t } from "i18next";
|
||||
import { InfoIcon, CheckCircle, DownloadIcon, XCircleIcon } from "lucide-react";
|
||||
import { CheckCircle, DownloadIcon, XCircleIcon } from "lucide-react";
|
||||
import { WHISPER_MODELS_OPTIONS } from "@/constants";
|
||||
import { useState, useContext, useEffect } from "react";
|
||||
import {
|
||||
@@ -36,54 +30,6 @@ type ModelType = {
|
||||
downloadState?: DownloadStateType;
|
||||
};
|
||||
|
||||
export const WhisperModelOptionsPanel = () => {
|
||||
const { EnjoyApp } = useContext(AppSettingsProviderContext);
|
||||
const { whisperConfig, refreshWhisperConfig } = useContext(
|
||||
AISettingsProviderContext
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
refreshWhisperConfig();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle>{t("whisperModel")}</CardTitle>
|
||||
<CardDescription>
|
||||
{t("chooseAIModelDependingOnYourHardware")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<WhisperModelOptions />
|
||||
</CardContent>
|
||||
|
||||
<CardFooter>
|
||||
<div className="text-xs flex items-start space-x-2">
|
||||
<InfoIcon className="mr-1.5 w-4 h-4" />
|
||||
<span className="flex-1 opacity-70">
|
||||
{t("yourModelsWillBeDownloadedTo", {
|
||||
path: whisperConfig.modelsPath,
|
||||
})}
|
||||
</span>
|
||||
<Button
|
||||
onClick={() => {
|
||||
EnjoyApp.shell.openPath(whisperConfig.modelsPath);
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
{t("open")}
|
||||
</Button>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const WhisperModelOptions = () => {
|
||||
const [selectingModel, setSelectingModel] = useState<ModelType | null>(null);
|
||||
const [availableModels, setAvailableModels] = useState<ModelType[]>([]);
|
||||
|
||||
@@ -2,11 +2,7 @@ import { t } from "i18next";
|
||||
import { useState, useContext, useEffect } from "react";
|
||||
import { Button, Progress } from "@renderer/components/ui";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
LoginForm,
|
||||
ChooseLibraryPathInput,
|
||||
WhisperModelOptionsPanel,
|
||||
} from "@renderer/components";
|
||||
import { LoginForm, ChooseLibraryPathInput } from "@renderer/components";
|
||||
import {
|
||||
AppSettingsProviderContext,
|
||||
AISettingsProviderContext,
|
||||
@@ -21,7 +17,7 @@ export default () => {
|
||||
AppSettingsProviderContext
|
||||
);
|
||||
const { whisperConfig } = useContext(AISettingsProviderContext);
|
||||
const totalSteps = 4;
|
||||
const totalSteps = 3;
|
||||
|
||||
useEffect(() => {
|
||||
validateCurrentStep();
|
||||
@@ -36,9 +32,6 @@ export default () => {
|
||||
setCurrentStepValid(!!libraryPath);
|
||||
break;
|
||||
case 3:
|
||||
setCurrentStepValid(true);
|
||||
break;
|
||||
case 4:
|
||||
setCurrentStepValid(initialized);
|
||||
break;
|
||||
default:
|
||||
@@ -68,10 +61,6 @@ export default () => {
|
||||
subtitle: t("whereYourResourcesAreStored"),
|
||||
},
|
||||
3: {
|
||||
title: t("AIModel"),
|
||||
subtitle: t("chooseAIModelToDownload"),
|
||||
},
|
||||
4: {
|
||||
title: t("finish"),
|
||||
subtitle: t("youAreReadyToGo"),
|
||||
},
|
||||
@@ -91,8 +80,7 @@ export default () => {
|
||||
<div className="flex-1 flex justify-center items-center">
|
||||
{currentStep == 1 && <LoginForm />}
|
||||
{currentStep == 2 && <ChooseLibraryPathInput />}
|
||||
{currentStep == 3 && <WhisperModelOptionsPanel />}
|
||||
{currentStep == 4 && (
|
||||
{currentStep == 3 && (
|
||||
<div className="flex justify-center items-center">
|
||||
<CheckCircle2Icon className="text-green-500 w-24 h-24" />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user