add sample for checking whisper & ffmpeg
This commit is contained in:
BIN
enjoy/samples/jfk.wav
Normal file
BIN
enjoy/samples/jfk.wav
Normal file
Binary file not shown.
@@ -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",
|
||||
|
||||
@@ -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 密钥",
|
||||
|
||||
@@ -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<boolean> {
|
||||
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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex items-start justify-between py-4">
|
||||
<div className="">
|
||||
@@ -318,32 +326,37 @@ const WhisperSettings = () => {
|
||||
<div className="text-sm text-muted-foreground">{whisperModel}</div>
|
||||
</div>
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="secondary" size="sm">
|
||||
{t("edit")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>{t("sttAiModel")}</DialogHeader>
|
||||
<DialogDescription>
|
||||
{t("chooseAIModelDependingOnYourHardware")}
|
||||
</DialogDescription>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button onClick={handleCheck} variant="secondary" size="sm">
|
||||
{t("check")}
|
||||
</Button>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="secondary" size="sm">
|
||||
{t("edit")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>{t("sttAiModel")}</DialogHeader>
|
||||
<DialogDescription>
|
||||
{t("chooseAIModelDependingOnYourHardware")}
|
||||
</DialogDescription>
|
||||
|
||||
<WhisperModelOptions />
|
||||
<WhisperModelOptions />
|
||||
|
||||
<DialogFooter>
|
||||
<div className="text-xs opacity-70 flex items-start">
|
||||
<InfoIcon className="mr-1.5 w-4 h-4" />
|
||||
<span className="flex-1">
|
||||
{t("yourModelsWillBeDownloadedTo", {
|
||||
path: whisperModelsPath,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<DialogFooter>
|
||||
<div className="text-xs opacity-70 flex items-start">
|
||||
<InfoIcon className="mr-1.5 w-4 h-4" />
|
||||
<span className="flex-1">
|
||||
{t("yourModelsWillBeDownloadedTo", {
|
||||
path: whisperModelsPath,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
1
enjoy/src/types/enjoy-app.d.ts
vendored
1
enjoy/src/types/enjoy-app.d.ts
vendored
@@ -173,6 +173,7 @@ type EnjoyAppType = {
|
||||
whisper: {
|
||||
availableModels: () => Promise<string[]>;
|
||||
downloadModel: (name: string) => Promise<any>;
|
||||
check: () => Promise<boolean>;
|
||||
transcribe: (
|
||||
blob: { type: string; arrayBuffer: ArrayBuffer },
|
||||
prompt?: string
|
||||
|
||||
@@ -52,6 +52,10 @@ export default defineConfig({
|
||||
src: "src/main/db/migrations/*",
|
||||
dest: "migrations",
|
||||
},
|
||||
{
|
||||
src: "samples/*",
|
||||
dest: "samples",
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user