import { createWriteStream } from "node:fs"; import { constants as fsConstants } from "node:fs"; import fs from "node:fs/promises"; import { get as httpsGet } from "node:https"; import type Electron from "electron"; import { WHISPER_MODEL_DIR, WHISPER_MODEL_DOWNLOAD_URL, WHISPER_SMALL_MODEL_PATH, } from "../constants"; export function sendWhisperModelDownloadProgress( webContents: Electron.WebContents, payload: { status: "idle" | "downloading" | "downloaded" | "error"; progress: number; path?: string | null; error?: string; }, ) { webContents.send("whisper-small-model-download-progress", payload); } export async function getWhisperSmallModelStatus() { try { await fs.access(WHISPER_SMALL_MODEL_PATH, fsConstants.R_OK); return { success: true, exists: true, path: WHISPER_SMALL_MODEL_PATH, }; } catch { return { success: true, exists: false, path: null, }; } } export function downloadFileWithProgress( url: string, destinationPath: string, onProgress: (progress: number) => void, ): Promise { const request = (currentUrl: string, redirectCount = 0): Promise => { return new Promise((resolve, reject) => { const req = httpsGet(currentUrl, { timeout: 30_000 }, (response) => { const statusCode = response.statusCode ?? 0; const location = response.headers.location; if (statusCode >= 300 && statusCode < 400 && location) { response.resume(); if (redirectCount >= 5) { reject(new Error("Too many redirects while downloading Whisper model.")); return; } const nextUrl = new URL(location, currentUrl).toString(); void request(nextUrl, redirectCount + 1) .then(resolve) .catch(reject); return; } if (statusCode < 200 || statusCode >= 300) { response.resume(); reject(new Error(`Whisper model download failed with status ${statusCode}.`)); return; } const totalBytes = Number.parseInt( String(response.headers["content-length"] ?? "0"), 10, ); let downloadedBytes = 0; const fileStream = createWriteStream(destinationPath); response.on("data", (chunk: Buffer) => { downloadedBytes += chunk.length; if (Number.isFinite(totalBytes) && totalBytes > 0) { onProgress(Math.min(100, Math.round((downloadedBytes / totalBytes) * 100))); } }); response.on("error", (error) => { fileStream.destroy(error); }); fileStream.on("error", (error) => { response.destroy(error); reject(error); }); fileStream.on("finish", () => { onProgress(100); resolve(); }); response.pipe(fileStream); }); req.on("error", reject); req.on("timeout", () => { req.destroy(new Error("Whisper model download timed out.")); }); }); }; return request(url); } export async function downloadWhisperSmallModel( webContents: Electron.WebContents, ): Promise { await fs.mkdir(WHISPER_MODEL_DIR, { recursive: true }); const tempPath = `${WHISPER_SMALL_MODEL_PATH}.download`; sendWhisperModelDownloadProgress(webContents, { status: "downloading", progress: 0, path: null, }); try { await fs.rm(tempPath, { force: true }); await downloadFileWithProgress(WHISPER_MODEL_DOWNLOAD_URL, tempPath, (progress) => { sendWhisperModelDownloadProgress(webContents, { status: "downloading", progress, path: null, }); }); await fs.rename(tempPath, WHISPER_SMALL_MODEL_PATH); sendWhisperModelDownloadProgress(webContents, { status: "downloaded", progress: 100, path: WHISPER_SMALL_MODEL_PATH, }); return WHISPER_SMALL_MODEL_PATH; } catch (error) { await fs.rm(tempPath, { force: true }).catch(() => undefined); sendWhisperModelDownloadProgress(webContents, { status: "error", progress: 0, path: null, error: String(error), }); throw error; } } export async function deleteWhisperSmallModel(): Promise { await fs.rm(WHISPER_SMALL_MODEL_PATH, { force: true }); }