Skip to content

Commit

Permalink
feat: Core ML support
Browse files Browse the repository at this point in the history
  • Loading branch information
linxiaodong authored and buxuku committed Oct 11, 2024
1 parent 8fa8722 commit 3e5db28
Show file tree
Hide file tree
Showing 13 changed files with 496 additions and 102 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
- 支持选择模型下载源(国内镜像源或官方源)
- 支持自定义并发任务数量

## Core ML 支持

从 1.20.0 版本开始,在苹果芯片上,支持使用 Core ML 加速语音识别。对于之前安装过老版本的朋友,请先卸载老版本,然后重新安装新版本。并在设置界面里面,选择重新安装 `whisper.cpp`。即可正常使用 Core ML 加速。

## 翻译服务

Expand Down
4 changes: 4 additions & 0 deletions README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ This application retains all the features of the original [VideoSubtitleGenerato
- Option to choose model download source (domestic mirror or official source)
- Customizable number of concurrent tasks

## Core ML support

Starting from version 1.20.0, Core ML is supported on Apple Silicon, providing faster speech recognition. For users who have previously installed older versions, please uninstall the old version first and then reinstall the new version. Additionally, select "Reinstall Whisper" in the settings interface to enable Core ML acceleration.

## Translation Services

This project supports various translation services, including Baidu Translation, Volcano Engine Translation, DeepLX, local Ollama models, and OpenAI-style APIs. Using these services requires the appropriate API keys or configurations.
Expand Down
7 changes: 6 additions & 1 deletion main/helpers/storeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,9 @@ export function setupStoreHandlers() {
ipcMain.handle('getSettings', async () => {
return store.get('settings');
});
}

ipcMain.handle('clearConfig', async () => {
store.clear();
return true;
});
}
22 changes: 19 additions & 3 deletions main/helpers/systemInfoManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
downloadModelSync,
install,
makeWhisper,
reinstallWhisper,
} from './whisper';
import fs from 'fs';
import path from 'path';
Expand All @@ -24,15 +25,18 @@ export function setupSystemInfoManager(mainWindow: BrowserWindow) {
});

ipcMain.handle('deleteModel', async (event, modelName) => {
await deleteModel(modelName);
await deleteModel(modelName?.toLowerCase());
return true;
});

ipcMain.handle('downloadModel', async (event, { model, source }) => {
if (downloadingModels.has(model)) {
return false; // 如果模型已经在下载中,则返回 false
}

downloadingModels.add(model);
const onProcess = (data) => {
const match = data?.match?.(/(\d+)/);
console.log(match, model, 'match');
if (match) {
event.sender.send('downloadProgress', model, +match[1]);
}
Expand All @@ -43,12 +47,12 @@ export function setupSystemInfoManager(mainWindow: BrowserWindow) {
try {
await downloadModelSync(model?.toLowerCase(), source, onProcess);
downloadingModels.delete(model);
return true;
} catch (error) {
event.sender.send('message', 'download error, please try again');
downloadingModels.delete(model);
return false;
}
return true;
});

ipcMain.on('installWhisper', (event, source) => {
Expand Down Expand Up @@ -81,4 +85,16 @@ export function setupSystemInfoManager(mainWindow: BrowserWindow) {

return false;
});

ipcMain.handle('reinstallWhisper', async (event) => {
try {
await reinstallWhisper();
return true;
} catch (error) {
console.error('删除 whisper.cpp 目录失败:', error);
event.sender.send('message', `删除 whisper.cpp 目录失败: ${error.message}`);
return false;
}
});

}
1 change: 0 additions & 1 deletion main/helpers/translate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export default async function translate(
sourceLanguage,
targetLanguage,
} = formData || {};
console.log(formData, 'formData');

const renderContentTemplate = contentTemplate[translateContent];
const proof = provider;
Expand Down
4 changes: 4 additions & 0 deletions main/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export const isDarwin = () => os.platform() === "darwin";

export const isWin32 = () => os.platform() === "win32";

export const isAppleSilicon = () => {
return os.platform() === 'darwin' && os.arch() === 'arm64';
};

export const getExtraResourcesPath = () => {
const isProd = process.env.NODE_ENV === "production";
return isProd
Expand Down
100 changes: 84 additions & 16 deletions main/helpers/whisper.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { exec, spawn } from "child_process";
import { app } from "electron";
import path from "path";
import fs from "fs";
import git from "isomorphic-git";
import http from "isomorphic-git/http/node";
import { isWin32 } from "./utils";
import { isAppleSilicon, isWin32 } from "./utils";
import { BrowserWindow, DownloadItem } from 'electron';
import decompress from 'decompress';
import fs from 'fs-extra';

export const getPath = (key?: string) => {
const userDataPath = app.getPath("userData");
Expand Down Expand Up @@ -95,7 +96,13 @@ export const makeWhisper = (event) => {
event.sender.send("message", "whisper.cpp 未下载,请先下载 whisper.cpp");
}
event.sender.send("beginMakeWhisper", true);
exec(`make -C "${whisperPath}"`, (err, stdout) => {

// 根据芯片类型选择编译命令
const makeCommand = isAppleSilicon()
? `WHISPER_COREML=1 make -j -C "${whisperPath}"`
: `make -C "${whisperPath}"`;

exec(makeCommand, (err, stdout) => {
if (err) {
event.sender.send("message", err);
} else {
Expand All @@ -112,51 +119,96 @@ export const makeWhisper = (event) => {
export const deleteModel = async (model) => {
const modelsPath = getPath("modelsPath");
const modelPath = path.join(modelsPath, `ggml-${model}.bin`);
const coreMLModelPath = path.join(modelsPath, `ggml-${model}-encoder.mlmodelc`);

return new Promise((resolve, reject) => {
if (fs.existsSync(modelPath)) {
fs.unlinkSync(modelPath);
try {
if (fs.existsSync(modelPath)) {
fs.unlinkSync(modelPath);
}
if (fs.existsSync(coreMLModelPath)) {
fs.removeSync(coreMLModelPath); // 递归删除目录
}
resolve("ok");
} catch (error) {
console.error('删除模型失败:', error);
reject(error);
}
resolve("ok");
});
};

export const downloadModelSync = async (model: string, source: string, onProcess: (message: string) => void) => {
const modelsPath = getPath("modelsPath");
const modelPath = path.join(modelsPath, `ggml-${model}.bin`);
const coreMLModelPath = path.join(modelsPath, `ggml-${model}-encoder.mlmodelc`);

if (fs.existsSync(modelPath)) {
if (fs.existsSync(modelPath) && (!isAppleSilicon() || fs.existsSync(coreMLModelPath))) {
return;
}
if (!checkWhisperInstalled()) {
throw Error("whisper.cpp 未安装,请先安装 whisper.cpp");
}

const url = `https://${source === 'huggingface' ? 'huggingface.co' : 'hf-mirror.com'}/ggerganov/whisper.cpp/resolve/main/ggml-${model}.bin`;
const baseUrl = `https://${source === 'huggingface' ? 'huggingface.co' : 'hf-mirror.com'}/ggerganov/whisper.cpp/resolve/main`;
const url = `${baseUrl}/ggml-${model}.bin`;
const coreMLUrl = `${baseUrl}/ggml-${model}-encoder.mlmodelc.zip`;

return new Promise((resolve, reject) => {
const win = new BrowserWindow({ show: false });
let downloadCount = 0;
const totalDownloads = isAppleSilicon() ? 2 : 1;
let totalBytes = { normal: 0, coreML: 0 };
let receivedBytes = { normal: 0, coreML: 0 };

const willDownloadHandler = (event, item: DownloadItem) => {
if (item.getFilename() !== `ggml-${model}.bin`) {
const isCoreML = item.getFilename().includes('-encoder.mlmodelc');

// 检查是否为当前模型的下载项
if (!item.getFilename().includes(`ggml-${model}`)) {
return; // 忽略不匹配的下载项
}

item.setSavePath(modelPath);
if (isCoreML && !isAppleSilicon()) {
item.cancel();
return;
}
const savePath = isCoreML ? path.join(modelsPath, `ggml-${model}-encoder.mlmodelc.zip`) : modelPath;
item.setSavePath(savePath);

const type = isCoreML ? 'coreML' : 'normal';
totalBytes[type] = item.getTotalBytes();

item.on('updated', (event, state) => {
if (state === 'progressing' && !item.isPaused()) {
const percent = item.getReceivedBytes() / item.getTotalBytes() * 100;
receivedBytes[type] = item.getReceivedBytes();
const totalProgress = (receivedBytes.normal + receivedBytes.coreML) / (totalBytes.normal + totalBytes.coreML);
const percent = totalProgress * 100;
onProcess(`${model}: ${percent.toFixed(2)}%`);
}
});

item.once('done', (event, state) => {
item.once('done', async (event, state) => {
if (state === 'completed') {
onProcess(`${model} 完成`);
cleanup();
resolve(1);
downloadCount++;

if (isCoreML) {
try {
const zipPath = path.join(modelsPath, `ggml-${model}-encoder.mlmodelc.zip`);
await decompress(zipPath, modelsPath);
fs.unlinkSync(zipPath); // 删除zip文件
onProcess(`Core ML ${model} 解压完成`);
} catch (error) {
console.error('解压Core ML模型失败:', error);
reject(new Error(`解压Core ML模型失败: ${error.message}`));
}
}

if (downloadCount === totalDownloads) {
onProcess(`${model} 下载完成`);
cleanup();
resolve(1);
}
} else {
fs.unlink(modelPath, () => {});
cleanup();
reject(new Error(`${model} download error: ${state}`));
}
Expand All @@ -170,6 +222,9 @@ export const downloadModelSync = async (model: string, source: string, onProcess

win.webContents.session.on('will-download', willDownloadHandler);
win.webContents.downloadURL(url);
if (isAppleSilicon()) {
win.webContents.downloadURL(coreMLUrl);
}
});
};

Expand Down Expand Up @@ -197,3 +252,16 @@ export async function checkOpenAiWhisper(): Promise<boolean> {
});
});
}

export const reinstallWhisper = async () => {
const whisperPath = getPath("whisperPath");

// 删除现有的 whisper.cpp 目录
try {
await fs.remove(whisperPath);
return true;
} catch (error) {
console.error('删除 whisper.cpp 目录失败:', error);
throw new Error('删除 whisper.cpp 目录失败');
}
};
1 change: 0 additions & 1 deletion next-i18next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ module.exports = {
defaultLocale: 'zh',
locales: ['zh', 'en'],
},
debug: process.env.NODE_ENV === 'development',
reloadOnPrerender: process.env.NODE_ENV === 'development',
localePath:
typeof window === 'undefined'
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"private": true,
"name": "video-subtitle-master",
"description": "视频转字幕,字幕翻译软件",
"version": "1.1.0",
"version": "1.2.0",
"author": "buxuku <[email protected]>",
"main": "app/background.js",
"scripts": {
Expand Down Expand Up @@ -31,10 +31,12 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"crypto": "^1.0.1",
"decompress": "^4.2.1",
"electron-serve": "^1.3.0",
"electron-store": "^8.2.0",
"ffmpeg-static": "^5.2.0",
"fluent-ffmpeg": "^2.1.2",
"fs-extra": "^11.2.0",
"i18next": "^23.15.2",
"isomorphic-git": "^1.25.10",
"lodash": "^4.17.21",
Expand All @@ -52,6 +54,7 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"@types/node": "^20.11.16",
"@types/react": "^18.2.52",
"autoprefixer": "^10.4.19",
Expand Down
Loading

0 comments on commit 3e5db28

Please sign in to comment.