diff --git a/enjoy/samples/jfk.wav b/enjoy/samples/jfk.wav new file mode 100644 index 00000000..3184d372 Binary files /dev/null and b/enjoy/samples/jfk.wav differ diff --git a/enjoy/src/i18n/en.json b/enjoy/src/i18n/en.json index 14fa5daa..bd66a496 100644 --- a/enjoy/src/i18n/en.json +++ b/enjoy/src/i18n/en.json @@ -290,6 +290,9 @@ "advanced": "Advanced", "language": "Language", "sttAiModel": "STT AI model", + "checkingWhisper": "Checking whisper status", + "whisperIsWorkingGood": "Whisper is working good", + "whisperIsNotWorking": "Whisper is not working", "relaunchIsNeededAfterChanged": "Relaunch is needed after changed", "openaiKeySaved": "OpenAI key saved", "openaiKeyRequired": "OpenAI key required", diff --git a/enjoy/src/i18n/zh-CN.json b/enjoy/src/i18n/zh-CN.json index a60f1171..d58c4f65 100644 --- a/enjoy/src/i18n/zh-CN.json +++ b/enjoy/src/i18n/zh-CN.json @@ -193,7 +193,7 @@ "ffmpegCheck": "FFmpeg 检查", "check": "检查", "ffmpegCommandIsWorking": "FFmpeg 命令正常工作", - "ffmpegCommandIsNotWorking": "FFmpeg 命令无法正常工作", + "ffmpegCommandIsNotWorking": "FFmpeg 命令无法正常工作,请尝试重新安装 FFmpeg", "scan": "查找", "checkIfFfmpegIsInstalled": "检查 FFmpeg 是否已正确安装", "ffmpegFoundAt": "检测到 FFmpeg 命令: {{path}}", @@ -289,6 +289,9 @@ "advancedSettings": "高级设置", "language": "语言", "sttAiModel": "语音转文本 AI 模型", + "checkingWhisper": "正在检查 Whisper", + "whisperIsWorkingGood": "Whisper 正常工作", + "whisperIsNotWorking": "Whisper 无法正常工作,请尝试更换模型后重试,或联系开发者", "relaunchIsNeededAfterChanged": "更改后需要重新启动", "openaiKeySaved": "OpenAI 密钥已保存", "openaiKeyRequired": "未提供 OpenAI 密钥", diff --git a/enjoy/src/main/ffmpeg.ts b/enjoy/src/main/ffmpeg.ts index 484031cc..287d7f48 100644 --- a/enjoy/src/main/ffmpeg.ts +++ b/enjoy/src/main/ffmpeg.ts @@ -9,6 +9,7 @@ import downloader from "@main/downloader"; import storage from "@main/storage"; import readdirp from "readdirp"; import { t } from "i18next"; +import { uniq } from "lodash"; const logger = log.scope("ffmpeg"); export default class FfmpegWrapper { @@ -35,13 +36,14 @@ export default class FfmpegWrapper { } checkCommand(): Promise { + const sampleFile = path.join(__dirname, "samples", "jfk.wav"); return new Promise((resolve, _reject) => { - this.ffmpeg.getAvailableFormats((err, formats) => { + this.ffmpeg.input(sampleFile).getAvailableFormats((err, _formats) => { if (err) { logger.error("Command not valid:", err); resolve(false); } else { - logger.info("Command valid, available formats:", formats); + logger.info("Command valid, available formats"); resolve(true); } }); @@ -365,7 +367,20 @@ export const discoverFfmpeg = async () => { let ffmpegPath: string; let ffprobePath: string; const libraryFfmpegPath = path.join(settings.libraryPath(), "ffmpeg"); - const scanDirs = [...COMMAND_SCAN_DIR[platform], libraryFfmpegPath]; + + let scanDirs = [...COMMAND_SCAN_DIR[platform], libraryFfmpegPath]; + + const currentFfmpegPath = settings.ffmpegConfig().ffmpegPath as string; + const currentFfprobePath = settings.ffmpegConfig().ffprobePath as string; + + if (currentFfmpegPath) { + scanDirs.push(path.dirname(currentFfmpegPath)); + } + if (currentFfprobePath) { + scanDirs.push(path.dirname(currentFfprobePath)); + } + + scanDirs = uniq(scanDirs); await Promise.all( scanDirs.map(async (dir: string) => { @@ -425,6 +440,8 @@ export const discoverFfmpeg = async () => { export const COMMAND_SCAN_DIR: { [key: string]: string[] } = { darwin: [ + "/usr/bin", + "/usr/local/bin", "/Applications", process.env.HOME + "/Applications", "/opt/homebrew/bin", diff --git a/enjoy/src/main/whisper.ts b/enjoy/src/main/whisper.ts index c39b5616..2bb9d7ad 100644 --- a/enjoy/src/main/whisper.ts +++ b/enjoy/src/main/whisper.ts @@ -17,6 +17,51 @@ class Whipser { constructor() {} + async check() { + const sampleFile = path.join(__dirname, "samples", "jfk.wav"); + const tmpDir = settings.cachePath(); + const outputFile = path.join(tmpDir, "jfk.json"); + return new Promise((resolve, reject) => { + const commands = [ + `"${this.binMain}"`, + `--file "${sampleFile}"`, + `--model "${settings.whisperModelPath()}"`, + "--output-json", + `--output-file "${path.join(tmpDir, "jfk")}"`, + ]; + logger.debug(`Running command: ${commands.join(" ")}`); + exec( + commands.join(" "), + { + timeout: PROCESS_TIMEOUT, + }, + (error, stdout, stderr) => { + if (error) { + logger.error("error", error); + } + + if (stderr) { + logger.error("stderr", stderr); + } + + if (stdout) { + logger.debug(stdout); + } + + if (fs.existsSync(outputFile)) { + resolve(true); + } else { + reject( + error || + new Error(stderr || "Whisper check failed: unknown error") + .message + ); + } + } + ); + }); + } + async transcribeBlob( blob: { type: string; arrayBuffer: ArrayBuffer }, prompt?: string @@ -200,6 +245,15 @@ class Whipser { }); }); + ipcMain.handle("whisper-check", async (event) => { + return this.check().catch((err) => { + event.sender.send("on-notification", { + type: "error", + message: err.message, + }); + }); + }); + ipcMain.handle("whisper-transcribe", async (event, blob, prompt) => { try { return await this.transcribeBlob(blob, prompt); diff --git a/enjoy/src/preload.ts b/enjoy/src/preload.ts index 9d53e6f2..14b130ec 100644 --- a/enjoy/src/preload.ts +++ b/enjoy/src/preload.ts @@ -339,6 +339,9 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", { downloadModel: (name: string) => { return ipcRenderer.invoke("whisper-download-model", name); }, + check: () => { + return ipcRenderer.invoke("whisper-check"); + }, transcribe: ( blob: { type: string; arrayBuffer: ArrayBuffer }, prompt?: string @@ -406,5 +409,5 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", { save: (id: string, data: WaveFormDataType) => { return ipcRenderer.invoke("waveforms-save", id, data); }, - } + }, }); diff --git a/enjoy/src/renderer/components/preferences/basic-settings.tsx b/enjoy/src/renderer/components/preferences/basic-settings.tsx index 01c61550..671d4de8 100644 --- a/enjoy/src/renderer/components/preferences/basic-settings.tsx +++ b/enjoy/src/renderer/components/preferences/basic-settings.tsx @@ -307,10 +307,18 @@ const FfmpegSettings = () => { }; const WhisperSettings = () => { - const { whisperModel, whisperModelsPath } = useContext( + const { whisperModel, whisperModelsPath, EnjoyApp } = useContext( AppSettingsProviderContext ); + const handleCheck = async () => { + toast.promise(EnjoyApp.whisper.check(), { + loading: t("checkingWhisper"), + success: t("whisperIsWorkingGood"), + error: t("whisperIsNotWorking"), + }); + }; + return (
@@ -318,32 +326,37 @@ const WhisperSettings = () => {
{whisperModel}
- - - - - - {t("sttAiModel")} - - {t("chooseAIModelDependingOnYourHardware")} - +
+ + + + + + + {t("sttAiModel")} + + {t("chooseAIModelDependingOnYourHardware")} + - + - -
- - - {t("yourModelsWillBeDownloadedTo", { - path: whisperModelsPath, - })} - -
-
-
-
+ +
+ + + {t("yourModelsWillBeDownloadedTo", { + path: whisperModelsPath, + })} + +
+
+ +
+
); }; diff --git a/enjoy/src/types/enjoy-app.d.ts b/enjoy/src/types/enjoy-app.d.ts index b7daf5ec..2a463ed7 100644 --- a/enjoy/src/types/enjoy-app.d.ts +++ b/enjoy/src/types/enjoy-app.d.ts @@ -173,6 +173,7 @@ type EnjoyAppType = { whisper: { availableModels: () => Promise; downloadModel: (name: string) => Promise; + check: () => Promise; transcribe: ( blob: { type: string; arrayBuffer: ArrayBuffer }, prompt?: string diff --git a/enjoy/vite.main.config.mts b/enjoy/vite.main.config.mts index cbc49259..f6eeff09 100644 --- a/enjoy/vite.main.config.mts +++ b/enjoy/vite.main.config.mts @@ -52,6 +52,10 @@ export default defineConfig({ src: "src/main/db/migrations/*", dest: "migrations", }, + { + src: "samples/*", + dest: "samples", + }, ], }), ],