"use strict"; const electron = require("electron"); const path = require("path"); const fs = require("fs"); const utils = require("@electron-toolkit/utils"); const child_process = require("child_process"); const iconv = require("iconv-lite"); 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"); async function detectHardware() { return new Promise((resolve) => { const smi = child_process.spawn("nvidia-smi"); smi.on("error", () => resolve({ gpu: false, name: "CPU" })); smi.on("close", (code) => { if (code === 0) { resolve({ gpu: true, name: "NVIDIA GPU (CUDA)" }); } else { resolve({ gpu: false, name: "CPU" }); } }); }); } function srtTimeToMs(timeStr) { const match = timeStr.match(/(\d+):(\d+):(\d+)[,.](\d+)/); if (!match) return 0; const [, h, m, s, ms] = match; return parseInt(h) * 36e5 + parseInt(m) * 6e4 + parseInt(s) * 1e3 + parseInt(ms); } function msToSrtTime(ms) { const h = Math.floor(ms / 36e5); ms %= 36e5; const m = Math.floor(ms / 6e4); ms %= 6e4; const s = Math.floor(ms / 1e3); const rem = ms % 1e3; return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")},${String(rem).padStart(3, "0")}`; } function fixSrt(srtContent) { const normalizedContent = srtContent.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); const blocks = normalizedContent.trim().split(/\n\n+/); const entries = []; for (const block of blocks) { const lines = block.trim().split("\n"); if (lines.length < 2) continue; const timeLine = lines.find((l) => l.includes("-->")); if (!timeLine) continue; const textLines = lines.filter((l) => !l.includes("-->") && !/^\d+$/.test(l.trim())); const text = textLines.join(" ").trim(); if (!text) continue; const [startStr, endStr] = timeLine.split("-->").map((s) => s.trim()); const startMs = srtTimeToMs(startStr); let endMs = srtTimeToMs(endStr); entries.push({ startMs, endMs, text }); } const merged = []; let i = 0; while (i < entries.length) { const entry = { ...entries[i] }; if (entry.endMs <= entry.startMs && i + 1 < entries.length) { const next = entries[i + 1]; entry.text = (entry.text + " " + next.text).trim(); entry.endMs = next.endMs; i += 2; } else { i++; } if (entry.endMs <= entry.startMs) { entry.endMs = entry.startMs + 3e3; } merged.push(entry); } for (let j = 1; j < merged.length; j++) { if (merged[j].startMs < merged[j - 1].endMs) { merged[j].startMs = merged[j - 1].endMs; } if (merged[j].endMs <= merged[j].startMs) { merged[j].endMs = merged[j].startMs + 3e3; } } let srt = ""; merged.forEach((entry, idx) => { srt += `${idx + 1} `; srt += `${msToSrtTime(entry.startMs)} --> ${msToSrtTime(entry.endMs)} `; srt += `${entry.text} `; }); return srt.trim() + "\n"; } function transcribe(filePath, options, onProgress, onData) { return new Promise((resolve, reject) => { const { model = "small", language = "tr", format = "srt" } = options; const whisperPath = path.join(BIN_PATH, "whisper.exe"); const modelPath = path.join(BIN_PATH, "models", `ggml-${model}.bin`); if (!fs.existsSync(whisperPath)) { return reject(new Error(`Whisper executable not found at: ${whisperPath}`)); } if (!fs.existsSync(modelPath)) { return reject(new Error(`Model not found at: ${modelPath}. Run: node scripts/setup-binaries.js`)); } const whisperArgs = ["-m", modelPath, "-f", filePath]; if (format === "srt") whisperArgs.push("-osrt"); else if (format === "vtt") whisperArgs.push("-ovtt"); else if (format === "txt") whisperArgs.push("-otxt"); if (language && language !== "auto") { whisperArgs.push("-l", language); } console.log("[Voicext] Whisper args:", whisperArgs.join(" ")); const whisper = child_process.spawn(whisperPath, whisperArgs); let stderrOutput = ""; whisper.stdout.on("data", (data) => { const text = iconv.decode(data, "cp857"); console.log("[Whisper stdout]", text); onData(text); }); whisper.stderr.on("data", (data) => { const text = iconv.decode(data, "cp857"); stderrOutput += text; console.log("[Whisper stderr]", text); if (text.includes("Loaded model")) { onProgress(20); } else if (text.includes("Loaded audio") || text.includes("source reader")) { onProgress(40); } else if (text.includes("RunComplete") || text.includes("CPU Tasks")) { onProgress(80); } else if (text.includes("Memory Usage")) { onProgress(95); } }); whisper.on("error", (err) => { console.error("[Voicext] Failed to start Whisper process:", err); reject(new Error(`Failed to start Whisper: ${err.message}`)); }); whisper.on("close", (code) => { console.log(`[Voicext] Whisper exited with code: ${code}`); const expectedOutput = filePath.replace(/\.[^/.]+$/, `.${format}`); if (code === 0) { if (format === "srt" && fs.existsSync(expectedOutput)) { try { const raw = fs.readFileSync(expectedOutput, "utf-8"); const fixed = fixSrt(raw); fs.writeFileSync(expectedOutput, fixed, "utf-8"); console.log("[Voicext] SRT post-processed and fixed"); } catch (e) { console.warn("[Voicext] SRT post-processing warning:", e.message); } } if (!fs.existsSync(expectedOutput)) { const baseName = path.basename(filePath).replace(/\.[^/.]+$/, `.${format}`); const altOutput = path.join(process.cwd(), baseName); if (fs.existsSync(altOutput)) { if (format === "srt") { const raw = fs.readFileSync(altOutput, "utf-8"); const fixed = fixSrt(raw); fs.writeFileSync(expectedOutput, fixed, "utf-8"); } else { fs.copyFileSync(altOutput, expectedOutput); } fs.unlinkSync(altOutput); } } onProgress(100); resolve({ success: true, outputPath: expectedOutput }); } else { reject(new Error(`Whisper failed (code ${code}). Details: ${stderrOutput}`)); } }); }); } function createWindow() { const mainWindow = new electron.BrowserWindow({ width: 1100, height: 670, show: false, autoHideMenuBar: true, ...process.platform === "linux" ? { icon } : {}, webPreferences: { preload: path.join(__dirname, "../preload/index.js"), sandbox: false }, frame: false, transparent: true, backgroundColor: "#00000000" }); mainWindow.on("ready-to-show", () => { mainWindow.show(); }); mainWindow.webContents.setWindowOpenHandler((details) => { electron.shell.openExternal(details.url); return { action: "deny" }; }); if (utils.is.dev && process.env["ELECTRON_RENDERER_URL"]) { mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]); } else { mainWindow.loadFile(path.join(__dirname, "../renderer/index.html")); } electron.ipcMain.handle("start-transcription", async (event, { filePath, options }) => { try { const result = await transcribe( filePath, options, (progress) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send("transcription-progress", progress); } }, (data) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send("transcription-data", data); } } ); return result; } catch (error) { console.error("Transcription Error:", error); throw error; } }); electron.ipcMain.handle("detect-hardware", async () => { return await detectHardware(); }); electron.ipcMain.on("window-controls", (event, action) => { const win = electron.BrowserWindow.fromWebContents(event.sender); if (action === "minimize") win.minimize(); if (action === "maximize") win.isMaximized() ? win.unmaximize() : win.maximize(); if (action === "close") win.close(); }); electron.ipcMain.on("open-explorer", (event, 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(() => { utils.electronApp.setAppUserModelId("com.voicext.app"); electron.app.on("browser-window-created", (_, window) => { utils.optimizer.watchWindowShortcuts(window); }); createWindow(); electron.app.on("activate", function() { if (electron.BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); electron.app.on("window-all-closed", () => { if (process.platform !== "darwin") { electron.app.quit(); } });