feat: implement electron-based audio transcription service with whisper integration and srt processing

This commit is contained in:
Ümit Tunç
2026-04-23 10:00:41 +03:00
parent 3f1f4f4608
commit e267fca6cb
4 changed files with 65 additions and 12 deletions
+39 -1
View File
@@ -1,9 +1,9 @@
"use strict"; "use strict";
const electron = require("electron"); const electron = require("electron");
const path = require("path"); const path = require("path");
const fs = require("fs");
const utils = require("@electron-toolkit/utils"); const utils = require("@electron-toolkit/utils");
const child_process = require("child_process"); const child_process = require("child_process");
const fs = require("fs");
const iconv = require("iconv-lite"); const iconv = require("iconv-lite");
const icon = path.join(__dirname, "../../resources/icon.png"); const icon = path.join(__dirname, "../../resources/icon.png");
const BIN_PATH = electron.app.isPackaged ? path.join(process.resourcesPath, "bin") : path.join(electron.app.getAppPath(), "bin"); const BIN_PATH = electron.app.isPackaged ? path.join(process.resourcesPath, "bin") : path.join(electron.app.getAppPath(), "bin");
@@ -231,6 +231,44 @@ function createWindow() {
electron.ipcMain.on("open-explorer", (event, path2) => { electron.ipcMain.on("open-explorer", (event, path2) => {
electron.shell.showItemInFolder(path2); electron.shell.showItemInFolder(path2);
}); });
electron.ipcMain.handle("read-srt", async (event, filePath) => {
try {
const content = fs.readFileSync(filePath, "utf-8");
return content;
} catch (error) {
console.error("Read SRT Error:", error);
throw error;
}
});
electron.ipcMain.handle("save-srt", async (event, { filePath, content }) => {
try {
fs.writeFileSync(filePath, content, "utf-8");
return { success: true };
} catch (error) {
console.error("Save SRT Error:", error);
throw error;
}
});
electron.ipcMain.handle("select-file", async (event, filters) => {
const { dialog } = require("electron");
const result = await dialog.showOpenDialog({
properties: ["openFile"],
filters: filters || []
});
if (result.canceled) return null;
return result.filePaths[0];
});
electron.ipcMain.handle("optimize-srt", async (event, filePath) => {
try {
const content = fs.readFileSync(filePath, "utf-8");
const optimized = fixSrt(content);
fs.writeFileSync(filePath, optimized, "utf-8");
return { success: true, content: optimized };
} catch (error) {
console.error("Optimize SRT Error:", error);
throw error;
}
});
} }
electron.app.whenReady().then(() => { electron.app.whenReady().then(() => {
utils.electronApp.setAppUserModelId("com.voicext.app"); utils.electronApp.setAppUserModelId("com.voicext.app");
+5 -1
View File
@@ -7,7 +7,11 @@ const api = {
onTranscriptionProgress: (callback) => electron.ipcRenderer.on("transcription-progress", (_, progress) => callback(progress)), onTranscriptionProgress: (callback) => electron.ipcRenderer.on("transcription-progress", (_, progress) => callback(progress)),
onTranscriptionData: (callback) => electron.ipcRenderer.on("transcription-data", (_, data) => callback(data)), onTranscriptionData: (callback) => electron.ipcRenderer.on("transcription-data", (_, data) => callback(data)),
detectHardware: () => electron.ipcRenderer.invoke("detect-hardware"), detectHardware: () => electron.ipcRenderer.invoke("detect-hardware"),
openExplorer: (path) => electron.ipcRenderer.send("open-explorer", path) openExplorer: (path) => electron.ipcRenderer.send("open-explorer", path),
readSrt: (filePath) => electron.ipcRenderer.invoke("read-srt", filePath),
saveSrt: (filePath, content) => electron.ipcRenderer.invoke("save-srt", { filePath, content }),
selectFile: (filters) => electron.ipcRenderer.invoke("select-file", filters),
optimizeSrt: (filePath) => electron.ipcRenderer.invoke("optimize-srt", filePath)
}; };
if (process.contextIsolated) { if (process.contextIsolated) {
try { try {
+6 -6
View File
@@ -149,7 +149,7 @@ function App() {
</div> </div>
<nav className="nav-tabs"> <nav className="nav-tabs">
{['dashboard', 'models', 'queue', 'settings'].map((tab) => ( {['dashboard', 'models', 'editor', 'settings'].map((tab) => (
<div <div
key={tab} key={tab}
className={`nav-item ${activeTab === tab ? 'active' : ''}`} className={`nav-item ${activeTab === tab ? 'active' : ''}`}
@@ -157,7 +157,7 @@ function App() {
> >
{tab === 'dashboard' && <LayoutDashboard size={18} />} {tab === 'dashboard' && <LayoutDashboard size={18} />}
{tab === 'models' && <Box size={18} />} {tab === 'models' && <Box size={18} />}
{tab === 'queue' && <ListMusic size={18} />} {tab === 'editor' && <ListMusic size={18} />}
{tab === 'settings' && <Settings size={18} />} {tab === 'settings' && <Settings size={18} />}
{tab.charAt(0).toUpperCase() + tab.slice(1)} {tab.charAt(0).toUpperCase() + tab.slice(1)}
</div> </div>
@@ -307,9 +307,9 @@ function App() {
</motion.div> </motion.div>
)} )}
{activeTab === 'queue' && ( {activeTab === 'editor' && (
<motion.div <motion.div
key="queue" key="editor"
initial={{ opacity: 0, scale: 0.95 }} initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 1.05 }} exit={{ opacity: 0, scale: 1.05 }}
@@ -325,7 +325,7 @@ function App() {
<ListMusic size={80} color="white" style={{ position: 'relative', zIndex: 1 }} /> <ListMusic size={80} color="white" style={{ position: 'relative', zIndex: 1 }} />
</div> </div>
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
<h2>SRT EDITOR & QUEUE</h2> <h2>SRT EDITOR</h2>
<p style={{ color: 'var(--text-muted)', marginBottom: '20px' }}>Select an SRT file to fix timestamps and edit text.</p> <p style={{ color: 'var(--text-muted)', marginBottom: '20px' }}>Select an SRT file to fix timestamps and edit text.</p>
<button className="btn-primary" style={{ maxWidth: '300px', margin: '0 auto' }} onClick={handleSelectSrt}> <button className="btn-primary" style={{ maxWidth: '300px', margin: '0 auto' }} onClick={handleSelectSrt}>
Browse SRT Files Browse SRT Files
@@ -359,7 +359,7 @@ function App() {
onClose={() => setShowResult(null)} onClose={() => setShowResult(null)}
onEdit={(path) => { onEdit={(path) => {
setSelectedSrt(path) setSelectedSrt(path)
setActiveTab('queue') setActiveTab('editor')
}} }}
/> />
</div> </div>
+15 -4
View File
@@ -90,6 +90,7 @@ body {
flex-direction: column; flex-direction: column;
padding: 20px; padding: 20px;
padding-top: 0; padding-top: 0;
min-height: 0; /* Important for flex children to scroll */
} }
.header { .header {
@@ -662,21 +663,31 @@ body {
.segments-list { .segments-list {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding-right: 10px; padding-right: 15px;
margin-bottom: 20px; margin-bottom: 20px;
min-height: 0;
} }
.segments-list::-webkit-scrollbar { .segments-list::-webkit-scrollbar {
width: 6px; width: 8px;
} }
.segments-list::-webkit-scrollbar-track { .segments-list::-webkit-scrollbar-track {
background: transparent; background: rgba(255, 255, 255, 0.02);
border-radius: 10px;
} }
.segments-list::-webkit-scrollbar-thumb { .segments-list::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1); background: rgba(0, 242, 255, 0.2);
border-radius: 10px; border-radius: 10px;
border: 2px solid transparent;
background-clip: padding-box;
}
.segments-list::-webkit-scrollbar-thumb:hover {
background: rgba(0, 242, 255, 0.4);
border: 2px solid transparent;
background-clip: padding-box;
} }
.segments-header { .segments-header {