feat: implement electron-based audio transcription service with whisper integration and srt processing
This commit is contained in:
+39
-1
@@ -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");
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user