Skip to content

Commit eef7d9f

Browse files
author
linxiaodong
committed
feat: transalte provider control
1 parent f4d160e commit eef7d9f

26 files changed

+1226
-826
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
- 自定义翻译后的字幕文件内容,纯翻译结果,原字幕+翻译结果
2121
- 项目集成 `whisper.cpp`, 它对 apple silicon 进行了优化,有较快的生成速度
2222
- 项目集成了 `fluent-ffmpeg`, 无须安装 `ffmpeg`
23+
- 支持运行本地安装的 `whisper` 命令
2324

2425
## 翻译服务
2526

main/background.ts

Lines changed: 11 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,11 @@
11
import path from "path";
2-
import { app, dialog, ipcMain, shell } from "electron";
3-
import { exec } from "child_process";
2+
import { app } from "electron";
43
import serve from "electron-serve";
5-
import { createWindow } from "./helpers";
6-
import {
7-
install,
8-
makeWhisper,
9-
checkWhisperInstalled,
10-
getModelsInstalled,
11-
deleteModel,
12-
downloadModelSync,
13-
getPath,
14-
checkOpenAiWhisper,
15-
} from "./helpers/whisper";
16-
import { extractAudio } from "./helpers/ffmpeg";
17-
import translate from "./helpers/translate";
18-
import {
19-
getExtraResourcesPath,
20-
isWin32,
21-
renderTemplate,
22-
} from "./helpers/utils";
23-
import fs from "fs";
4+
import { createWindow } from "./helpers/create-window";
5+
import { setupIpcHandlers } from './helpers/ipcHandler';
6+
import { setupTaskProcessor } from './helpers/taskProcessor';
7+
import { setupSystemInfoManager } from './helpers/systemInfoManager';
8+
import { setupStoreHandlers } from './helpers/storeManager';
249

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

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

3621
const mainWindow = createWindow("main", {
3722
width: 1400,
38-
height: 980,
23+
height: 1040,
3924
webPreferences: {
4025
preload: path.join(__dirname, "preload.js"),
4126
},
@@ -48,180 +33,13 @@ if (isProd) {
4833
await mainWindow.loadURL(`http://localhost:${port}/home`);
4934
mainWindow.webContents.openDevTools();
5035
}
36+
setupStoreHandlers();
37+
setupIpcHandlers(mainWindow);
38+
setupTaskProcessor(mainWindow);
39+
setupSystemInfoManager(mainWindow);
5140
})();
5241

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

57-
ipcMain.on("message", async (event, arg) => {
58-
event.reply("message", `${arg} World!`);
59-
});
60-
61-
ipcMain.on("openDialog", async (event, arg) => {
62-
dialog
63-
.showOpenDialog({
64-
properties: ["openFile", "multiSelections"],
65-
filters: [{ name: "Movies", extensions: ["mkv", "avi", "mp4"] }],
66-
})
67-
.then((result) => {
68-
try {
69-
event.sender.send("file-selected", result.filePaths);
70-
} catch (error) {
71-
event.sender.send("message", error);
72-
}
73-
});
74-
});
75-
76-
ipcMain.on("handleTask", async (event, { files, formData }) => {
77-
const {
78-
model,
79-
sourceLanguage,
80-
targetLanguage,
81-
sourceSrtSaveFileName,
82-
translateProvider,
83-
saveSourceSrt,
84-
} = formData || {};
85-
const userPath = app.getPath("userData");
86-
const whisperPath = path.join(userPath, "whisper.cpp/");
87-
88-
try {
89-
for (const file of files) {
90-
const { filePath } = file;
91-
let directory = path.dirname(filePath);
92-
let fileName = path.basename(filePath, path.extname(filePath));
93-
const audioFile = path.join(directory, `${fileName}.wav`);
94-
const templateData = {
95-
fileName,
96-
sourceLanguage,
97-
targetLanguage,
98-
};
99-
const srtFile = path.join(
100-
directory,
101-
`${renderTemplate(
102-
saveSourceSrt ? sourceSrtSaveFileName : "${fileName}-temp",
103-
templateData,
104-
)}`,
105-
);
106-
const whisperModel = model?.toLowerCase();
107-
event.sender.send("taskStatusChange", file, "extractAudio", "loading");
108-
await extractAudio(filePath, audioFile);
109-
event.sender.send("taskStatusChange", file, "extractAudio", "done");
110-
let mainPath = `${whisperPath}main`;
111-
if (isWin32()) {
112-
mainPath = path.join(
113-
getExtraResourcesPath(),
114-
"whisper-bin-x64",
115-
"main.exe",
116-
);
117-
}
118-
let runShell = `"${mainPath}" -m "${whisperPath}models/ggml-${whisperModel}.bin" -f "${audioFile}" -osrt -of "${srtFile}" -l ${sourceLanguage}`;
119-
const hasOpenAiWhiaper = await checkOpenAiWhisper();
120-
if (hasOpenAiWhiaper) {
121-
runShell = `whisper "${audioFile}" --model ${whisperModel} --device cuda --output_format srt --output_dir ${directory} --language ${sourceLanguage}`;
122-
}
123-
event.sender.send("taskStatusChange", file, "extractSubtitle", "loading");
124-
exec(runShell, async (error, stdout, stderr) => {
125-
if (error) {
126-
event.sender.send("message", error);
127-
return;
128-
}
129-
event.sender.send("taskStatusChange", file, "extractSubtitle", "done");
130-
fs.unlink(audioFile, (err) => {
131-
if (err) {
132-
console.log(err);
133-
}
134-
});
135-
if (translateProvider !== "-1") {
136-
event.sender.send(
137-
"taskStatusChange",
138-
file,
139-
"translateSubtitle",
140-
"loading",
141-
);
142-
143-
await translate(
144-
event,
145-
directory,
146-
fileName,
147-
`${srtFile}.srt`,
148-
formData,
149-
);
150-
event.sender.send(
151-
"taskStatusChange",
152-
file,
153-
"translateSubtitle",
154-
"done",
155-
);
156-
}
157-
if (!saveSourceSrt) {
158-
fs.unlink(`${srtFile}.srt`, (err) => {
159-
console.log(err);
160-
});
161-
}
162-
});
163-
}
164-
} catch (error) {
165-
event.sender.send("message", error);
166-
}
167-
});
168-
169-
ipcMain.on("getSystemInfo", (event, key) => {
170-
const res = {
171-
whisperInstalled: checkWhisperInstalled(),
172-
modelsInstalled: getModelsInstalled(),
173-
};
174-
event.sender.send("getSystemInfoComplete", res);
175-
});
176-
177-
ipcMain.on("installWhisper", (event, source) => {
178-
install(event, source);
179-
});
180-
181-
ipcMain.on("makeWhisper", (event) => {
182-
makeWhisper(event);
183-
});
184-
185-
186-
ipcMain.on("openUrl", (event, url) => {
187-
shell.openExternal(url);
188-
});
189-
190-
ipcMain.handle("deleteModel", async (event, modelName) => {
191-
await deleteModel(modelName);
192-
return true;
193-
});
194-
195-
let downloadingModels = new Set<string>();
196-
197-
ipcMain.handle("getSystemInfo", async (event, key) => {
198-
const res = {
199-
whisperInstalled: checkWhisperInstalled(),
200-
modelsInstalled: getModelsInstalled(),
201-
modelsPath: getPath("modelsPath"),
202-
downloadingModels: Array.from(downloadingModels),
203-
};
204-
return res;
205-
});
206-
207-
ipcMain.handle("downloadModel", async (event, { model, source }) => {
208-
downloadingModels.add(model);
209-
const onProcess = (data) => {
210-
const match = data?.match(/(\d+)%/);
211-
if (match) {
212-
event.sender.send("downloadProgress", model, +match[1]);
213-
}
214-
if(data?.includes("Done") || data?.includes("main")) {
215-
event.sender.send("downloadProgress", model, 100);
216-
}
217-
};
218-
try {
219-
await downloadModelSync(model?.toLowerCase(), source, onProcess);
220-
downloadingModels.delete(model);
221-
} catch (error) {
222-
event.sender.send("message", "下载失败,请切换下载源重试");
223-
downloadingModels.delete(model);
224-
return false;
225-
}
226-
return true;
227-
});

main/helpers/fileProcessor.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { app } from 'electron';
2+
import path from 'path';
3+
import fs from 'fs';
4+
import { exec } from 'child_process';
5+
import { extractAudio } from './ffmpeg';
6+
import translate from './translate';
7+
import { renderTemplate, isWin32, getExtraResourcesPath } from './utils';
8+
9+
export async function processFile(event, file, formData, hasOpenAiWhisper, translationProviders) {
10+
const {
11+
model,
12+
sourceLanguage,
13+
targetLanguage,
14+
sourceSrtSaveFileName,
15+
translateProvider,
16+
saveSourceSrt,
17+
} = formData || {};
18+
const userPath = app.getPath("userData");
19+
const whisperPath = path.join(userPath, "whisper.cpp/");
20+
21+
try {
22+
const { filePath } = file;
23+
let directory = path.dirname(filePath);
24+
let fileName = path.basename(filePath, path.extname(filePath));
25+
const audioFile = path.join(directory, `${fileName}.wav`);
26+
const templateData = {
27+
fileName,
28+
sourceLanguage,
29+
targetLanguage,
30+
};
31+
const srtFile = path.join(
32+
directory,
33+
`${renderTemplate(
34+
saveSourceSrt ? sourceSrtSaveFileName : "${fileName}-temp",
35+
templateData,
36+
)}`,
37+
);
38+
const whisperModel = model?.toLowerCase();
39+
40+
// 提取音频
41+
event.sender.send("taskStatusChange", file, "extractAudio", "loading");
42+
await extractAudio(filePath, audioFile);
43+
event.sender.send("taskStatusChange", file, "extractAudio", "done");
44+
45+
// 生成字幕
46+
let mainPath = `${whisperPath}main`;
47+
if (isWin32()) {
48+
mainPath = path.join(
49+
getExtraResourcesPath(),
50+
"whisper-bin-x64",
51+
"main.exe",
52+
);
53+
}
54+
let runShell = `"${mainPath}" -m "${whisperPath}models/ggml-${whisperModel}.bin" -f "${audioFile}" -osrt -of "${srtFile}" -l ${sourceLanguage}`;
55+
if (hasOpenAiWhisper) {
56+
runShell = `whisper "${audioFile}" --model ${whisperModel} --device cuda --output_format srt --output_dir ${directory} --language ${sourceLanguage}`;
57+
}
58+
event.sender.send("taskStatusChange", file, "extractSubtitle", "loading");
59+
60+
await new Promise((resolve, reject) => {
61+
exec(runShell, async (error, stdout, stderr) => {
62+
if (error) {
63+
reject(error);
64+
return;
65+
}
66+
event.sender.send("taskStatusChange", file, "extractSubtitle", "done");
67+
fs.unlink(audioFile, (err) => {
68+
if (err) {
69+
console.log(err);
70+
}
71+
});
72+
resolve(1);
73+
});
74+
});
75+
76+
// 翻译字幕
77+
if (translateProvider !== "-1") {
78+
event.sender.send(
79+
"taskStatusChange",
80+
file,
81+
"translateSubtitle",
82+
"loading",
83+
);
84+
const provider = translationProviders.find(p => p.id === translateProvider);
85+
await translate(
86+
event,
87+
directory,
88+
fileName,
89+
`${srtFile}.srt`,
90+
formData,
91+
provider
92+
);
93+
event.sender.send(
94+
"taskStatusChange",
95+
file,
96+
"translateSubtitle",
97+
"done",
98+
);
99+
}
100+
101+
// 清理临时文件
102+
if (!saveSourceSrt) {
103+
fs.unlink(`${srtFile}.srt`, (err) => {
104+
if (err) console.log(err);
105+
});
106+
}
107+
} catch (error) {
108+
event.sender.send("message", error);
109+
}
110+
}

main/helpers/ipcHandler.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ipcMain, BrowserWindow, dialog, shell } from 'electron';
2+
3+
export function setupIpcHandlers(mainWindow: BrowserWindow) {
4+
ipcMain.on("message", async (event, arg) => {
5+
event.reply("message", `${arg} World!`);
6+
});
7+
8+
ipcMain.on("openDialog", async (event) => {
9+
const result = await dialog.showOpenDialog({
10+
properties: ["openFile", "multiSelections"],
11+
filters: [{ name: "Movies", extensions: ["mkv", "avi", "mp4"] }],
12+
});
13+
14+
try {
15+
event.sender.send("file-selected", result.filePaths);
16+
} catch (error) {
17+
event.sender.send("message", error);
18+
}
19+
});
20+
21+
ipcMain.on("openUrl", (event, url) => {
22+
shell.openExternal(url);
23+
});
24+
}

0 commit comments

Comments
 (0)