Skip to content

Commit

Permalink
feat: transalte provider control
Browse files Browse the repository at this point in the history
  • Loading branch information
linxiaodong committed Sep 20, 2024
1 parent f4d160e commit eef7d9f
Show file tree
Hide file tree
Showing 26 changed files with 1,226 additions and 826 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- 自定义翻译后的字幕文件内容,纯翻译结果,原字幕+翻译结果
- 项目集成 `whisper.cpp`, 它对 apple silicon 进行了优化,有较快的生成速度
- 项目集成了 `fluent-ffmpeg`, 无须安装 `ffmpeg`
- 支持运行本地安装的 `whisper` 命令

## 翻译服务

Expand Down
204 changes: 11 additions & 193 deletions main/background.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,11 @@
import path from "path";
import { app, dialog, ipcMain, shell } from "electron";
import { exec } from "child_process";
import { app } from "electron";
import serve from "electron-serve";
import { createWindow } from "./helpers";
import {
install,
makeWhisper,
checkWhisperInstalled,
getModelsInstalled,
deleteModel,
downloadModelSync,
getPath,
checkOpenAiWhisper,
} from "./helpers/whisper";
import { extractAudio } from "./helpers/ffmpeg";
import translate from "./helpers/translate";
import {
getExtraResourcesPath,
isWin32,
renderTemplate,
} from "./helpers/utils";
import fs from "fs";
import { createWindow } from "./helpers/create-window";
import { setupIpcHandlers } from './helpers/ipcHandler';
import { setupTaskProcessor } from './helpers/taskProcessor';
import { setupSystemInfoManager } from './helpers/systemInfoManager';
import { setupStoreHandlers } from './helpers/storeManager';

const isProd = process.env.NODE_ENV === "production";

Expand All @@ -35,7 +20,7 @@ if (isProd) {

const mainWindow = createWindow("main", {
width: 1400,
height: 980,
height: 1040,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
},
Expand All @@ -48,180 +33,13 @@ if (isProd) {
await mainWindow.loadURL(`http://localhost:${port}/home`);
mainWindow.webContents.openDevTools();
}
setupStoreHandlers();
setupIpcHandlers(mainWindow);
setupTaskProcessor(mainWindow);
setupSystemInfoManager(mainWindow);
})();

app.on("window-all-closed", () => {
app.quit();
});

ipcMain.on("message", async (event, arg) => {
event.reply("message", `${arg} World!`);
});

ipcMain.on("openDialog", async (event, arg) => {
dialog
.showOpenDialog({
properties: ["openFile", "multiSelections"],
filters: [{ name: "Movies", extensions: ["mkv", "avi", "mp4"] }],
})
.then((result) => {
try {
event.sender.send("file-selected", result.filePaths);
} catch (error) {
event.sender.send("message", error);
}
});
});

ipcMain.on("handleTask", async (event, { files, formData }) => {
const {
model,
sourceLanguage,
targetLanguage,
sourceSrtSaveFileName,
translateProvider,
saveSourceSrt,
} = formData || {};
const userPath = app.getPath("userData");
const whisperPath = path.join(userPath, "whisper.cpp/");

try {
for (const file of files) {
const { filePath } = file;
let directory = path.dirname(filePath);
let fileName = path.basename(filePath, path.extname(filePath));
const audioFile = path.join(directory, `${fileName}.wav`);
const templateData = {
fileName,
sourceLanguage,
targetLanguage,
};
const srtFile = path.join(
directory,
`${renderTemplate(
saveSourceSrt ? sourceSrtSaveFileName : "${fileName}-temp",
templateData,
)}`,
);
const whisperModel = model?.toLowerCase();
event.sender.send("taskStatusChange", file, "extractAudio", "loading");
await extractAudio(filePath, audioFile);
event.sender.send("taskStatusChange", file, "extractAudio", "done");
let mainPath = `${whisperPath}main`;
if (isWin32()) {
mainPath = path.join(
getExtraResourcesPath(),
"whisper-bin-x64",
"main.exe",
);
}
let runShell = `"${mainPath}" -m "${whisperPath}models/ggml-${whisperModel}.bin" -f "${audioFile}" -osrt -of "${srtFile}" -l ${sourceLanguage}`;
const hasOpenAiWhiaper = await checkOpenAiWhisper();
if (hasOpenAiWhiaper) {
runShell = `whisper "${audioFile}" --model ${whisperModel} --device cuda --output_format srt --output_dir ${directory} --language ${sourceLanguage}`;
}
event.sender.send("taskStatusChange", file, "extractSubtitle", "loading");
exec(runShell, async (error, stdout, stderr) => {
if (error) {
event.sender.send("message", error);
return;
}
event.sender.send("taskStatusChange", file, "extractSubtitle", "done");
fs.unlink(audioFile, (err) => {
if (err) {
console.log(err);
}
});
if (translateProvider !== "-1") {
event.sender.send(
"taskStatusChange",
file,
"translateSubtitle",
"loading",
);

await translate(
event,
directory,
fileName,
`${srtFile}.srt`,
formData,
);
event.sender.send(
"taskStatusChange",
file,
"translateSubtitle",
"done",
);
}
if (!saveSourceSrt) {
fs.unlink(`${srtFile}.srt`, (err) => {
console.log(err);
});
}
});
}
} catch (error) {
event.sender.send("message", error);
}
});

ipcMain.on("getSystemInfo", (event, key) => {
const res = {
whisperInstalled: checkWhisperInstalled(),
modelsInstalled: getModelsInstalled(),
};
event.sender.send("getSystemInfoComplete", res);
});

ipcMain.on("installWhisper", (event, source) => {
install(event, source);
});

ipcMain.on("makeWhisper", (event) => {
makeWhisper(event);
});


ipcMain.on("openUrl", (event, url) => {
shell.openExternal(url);
});

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

let downloadingModels = new Set<string>();

ipcMain.handle("getSystemInfo", async (event, key) => {
const res = {
whisperInstalled: checkWhisperInstalled(),
modelsInstalled: getModelsInstalled(),
modelsPath: getPath("modelsPath"),
downloadingModels: Array.from(downloadingModels),
};
return res;
});

ipcMain.handle("downloadModel", async (event, { model, source }) => {
downloadingModels.add(model);
const onProcess = (data) => {
const match = data?.match(/(\d+)%/);
if (match) {
event.sender.send("downloadProgress", model, +match[1]);
}
if(data?.includes("Done") || data?.includes("main")) {
event.sender.send("downloadProgress", model, 100);
}
};
try {
await downloadModelSync(model?.toLowerCase(), source, onProcess);
downloadingModels.delete(model);
} catch (error) {
event.sender.send("message", "下载失败,请切换下载源重试");
downloadingModels.delete(model);
return false;
}
return true;
});
110 changes: 110 additions & 0 deletions main/helpers/fileProcessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { app } from 'electron';
import path from 'path';
import fs from 'fs';
import { exec } from 'child_process';
import { extractAudio } from './ffmpeg';
import translate from './translate';
import { renderTemplate, isWin32, getExtraResourcesPath } from './utils';

export async function processFile(event, file, formData, hasOpenAiWhisper, translationProviders) {
const {
model,
sourceLanguage,
targetLanguage,
sourceSrtSaveFileName,
translateProvider,
saveSourceSrt,
} = formData || {};
const userPath = app.getPath("userData");
const whisperPath = path.join(userPath, "whisper.cpp/");

try {
const { filePath } = file;
let directory = path.dirname(filePath);
let fileName = path.basename(filePath, path.extname(filePath));
const audioFile = path.join(directory, `${fileName}.wav`);
const templateData = {
fileName,
sourceLanguage,
targetLanguage,
};
const srtFile = path.join(
directory,
`${renderTemplate(
saveSourceSrt ? sourceSrtSaveFileName : "${fileName}-temp",
templateData,
)}`,
);
const whisperModel = model?.toLowerCase();

// 提取音频
event.sender.send("taskStatusChange", file, "extractAudio", "loading");
await extractAudio(filePath, audioFile);
event.sender.send("taskStatusChange", file, "extractAudio", "done");

// 生成字幕
let mainPath = `${whisperPath}main`;
if (isWin32()) {
mainPath = path.join(
getExtraResourcesPath(),
"whisper-bin-x64",
"main.exe",
);
}
let runShell = `"${mainPath}" -m "${whisperPath}models/ggml-${whisperModel}.bin" -f "${audioFile}" -osrt -of "${srtFile}" -l ${sourceLanguage}`;
if (hasOpenAiWhisper) {
runShell = `whisper "${audioFile}" --model ${whisperModel} --device cuda --output_format srt --output_dir ${directory} --language ${sourceLanguage}`;
}
event.sender.send("taskStatusChange", file, "extractSubtitle", "loading");

await new Promise((resolve, reject) => {
exec(runShell, async (error, stdout, stderr) => {
if (error) {
reject(error);
return;
}
event.sender.send("taskStatusChange", file, "extractSubtitle", "done");
fs.unlink(audioFile, (err) => {
if (err) {
console.log(err);
}
});
resolve(1);
});
});

// 翻译字幕
if (translateProvider !== "-1") {
event.sender.send(
"taskStatusChange",
file,
"translateSubtitle",
"loading",
);
const provider = translationProviders.find(p => p.id === translateProvider);
await translate(
event,
directory,
fileName,
`${srtFile}.srt`,
formData,
provider
);
event.sender.send(
"taskStatusChange",
file,
"translateSubtitle",
"done",
);
}

// 清理临时文件
if (!saveSourceSrt) {
fs.unlink(`${srtFile}.srt`, (err) => {
if (err) console.log(err);
});
}
} catch (error) {
event.sender.send("message", error);
}
}
24 changes: 24 additions & 0 deletions main/helpers/ipcHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ipcMain, BrowserWindow, dialog, shell } from 'electron';

export function setupIpcHandlers(mainWindow: BrowserWindow) {
ipcMain.on("message", async (event, arg) => {
event.reply("message", `${arg} World!`);
});

ipcMain.on("openDialog", async (event) => {
const result = await dialog.showOpenDialog({
properties: ["openFile", "multiSelections"],
filters: [{ name: "Movies", extensions: ["mkv", "avi", "mp4"] }],
});

try {
event.sender.send("file-selected", result.filePaths);
} catch (error) {
event.sender.send("message", error);
}
});

ipcMain.on("openUrl", (event, url) => {
shell.openExternal(url);
});
}
Loading

0 comments on commit eef7d9f

Please sign in to comment.