add sample for checking whisper & ffmpeg

This commit is contained in:
an-lee
2024-01-19 13:43:59 +08:00
parent d854ea0a02
commit 98d8061600
9 changed files with 128 additions and 30 deletions

BIN
enjoy/samples/jfk.wav Normal file

Binary file not shown.

View File

@@ -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",

View File

@@ -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 密钥",

View File

@@ -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",

View File

@@ -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);

View File

@@ -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);
},
}
},
});

View File

@@ -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>
);
};

View File

@@ -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

View File

@@ -52,6 +52,10 @@ export default defineConfig({
src: "src/main/db/migrations/*",
dest: "migrations",
},
{
src: "samples/*",
dest: "samples",
},
],
}),
],