diff --git a/desktop/src/api/clip.ts b/desktop/src/api/clip.ts index d2469e7b9..e69de29bb 100644 --- a/desktop/src/api/clip.ts +++ b/desktop/src/api/clip.ts @@ -1,54 +0,0 @@ -import { ipcRenderer } from "electron"; -import { writeStream } from "../services/fs"; -import { Model } from "../types"; -import { isExecError, parseExecError } from "../utils/error"; - -export async function computeImageEmbedding( - model: Model, - imageData: Uint8Array, -): Promise { - let tempInputFilePath = null; - try { - tempInputFilePath = await ipcRenderer.invoke("get-temp-file-path", ""); - const imageStream = new Response(imageData.buffer).body; - await writeStream(tempInputFilePath, imageStream); - const embedding = await ipcRenderer.invoke( - "compute-image-embedding", - model, - tempInputFilePath, - ); - return embedding; - } catch (err) { - if (isExecError(err)) { - const parsedExecError = parseExecError(err); - throw Error(parsedExecError); - } else { - throw err; - } - } finally { - if (tempInputFilePath) { - await ipcRenderer.invoke("remove-temp-file", tempInputFilePath); - } - } -} - -export async function computeTextEmbedding( - model: Model, - text: string, -): Promise { - try { - const embedding = await ipcRenderer.invoke( - "compute-text-embedding", - model, - text, - ); - return embedding; - } catch (err) { - if (isExecError(err)) { - const parsedExecError = parseExecError(err); - throw Error(parsedExecError); - } else { - throw err; - } - } -} diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 0f2180185..5199a1ff2 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -27,9 +27,11 @@ * just keep the entire preload setup in this single file. */ -import { contextBridge } from "electron"; +import { contextBridge, ipcRenderer } from "electron"; +import { existsSync } from "fs"; +import * as fs from "promise-fs"; +import { Readable } from "stream"; import { deleteDiskCache, openDiskCache } from "./api/cache"; -import { computeImageEmbedding, computeTextEmbedding } from "./api/clip"; import { getAppVersion, logToDisk, @@ -84,6 +86,157 @@ import { } from "./api/watch"; import { setupLogging } from "./utils/logging"; +/* preload: duplicated writeStream */ +/* Some of the code below has been duplicated to make this file self contained. + Enhancement: consider alternatives */ + +export const convertBrowserStreamToNode = ( + fileStream: ReadableStream, +) => { + const reader = fileStream.getReader(); + const rs = new Readable(); + + rs._read = async () => { + try { + const result = await reader.read(); + + if (!result.done) { + rs.push(Buffer.from(result.value)); + } else { + rs.push(null); + return; + } + } catch (e) { + rs.emit("error", e); + } + }; + + return rs; +}; + +export async function writeNodeStream( + filePath: string, + fileStream: NodeJS.ReadableStream, +) { + const writeable = fs.createWriteStream(filePath); + + fileStream.on("error", (error) => { + writeable.destroy(error); // Close the writable stream with an error + }); + + fileStream.pipe(writeable); + + await new Promise((resolve, reject) => { + writeable.on("finish", resolve); + writeable.on("error", async (e) => { + if (existsSync(filePath)) { + await fs.unlink(filePath); + } + reject(e); + }); + }); +} + +export async function writeStream( + filePath: string, + fileStream: ReadableStream, +) { + const readable = convertBrowserStreamToNode(fileStream); + await writeNodeStream(filePath, readable); +} + +// - + +/* preload: duplicated Model */ +export enum Model { + GGML_CLIP = "ggml-clip", + ONNX_CLIP = "onnx-clip", +} + +const computeImageEmbedding = async ( + model: Model, + imageData: Uint8Array, +): Promise => { + let tempInputFilePath = null; + try { + tempInputFilePath = await ipcRenderer.invoke("get-temp-file-path", ""); + const imageStream = new Response(imageData.buffer).body; + await writeStream(tempInputFilePath, imageStream); + const embedding = await ipcRenderer.invoke( + "compute-image-embedding", + model, + tempInputFilePath, + ); + return embedding; + } catch (err) { + if (isExecError(err)) { + const parsedExecError = parseExecError(err); + throw Error(parsedExecError); + } else { + throw err; + } + } finally { + if (tempInputFilePath) { + await ipcRenderer.invoke("remove-temp-file", tempInputFilePath); + } + } +}; + +export async function computeTextEmbedding( + model: Model, + text: string, +): Promise { + try { + const embedding = await ipcRenderer.invoke( + "compute-text-embedding", + model, + text, + ); + return embedding; + } catch (err) { + if (isExecError(err)) { + const parsedExecError = parseExecError(err); + throw Error(parsedExecError); + } else { + throw err; + } + } +} + +// - + +/* preload: duplicated CustomErrors */ +const CustomErrorsP = { + WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED: + "Windows native image processing is not supported", + INVALID_OS: (os: string) => `Invalid OS - ${os}`, + WAIT_TIME_EXCEEDED: "Wait time exceeded", + UNSUPPORTED_PLATFORM: (platform: string, arch: string) => + `Unsupported platform - ${platform} ${arch}`, + MODEL_DOWNLOAD_PENDING: + "Model download pending, skipping clip search request", + INVALID_FILE_PATH: "Invalid file path", + INVALID_CLIP_MODEL: (model: string) => `Invalid Clip model - ${model}`, +}; + +const isExecError = (err: any) => { + return err.message.includes("Command failed:"); +}; + +const parseExecError = (err: any) => { + const errMessage = err.message; + if (errMessage.includes("Bad CPU type in executable")) { + return CustomErrorsP.UNSUPPORTED_PLATFORM( + process.platform, + process.arch, + ); + } else { + return errMessage; + } +}; + +// - + setupLogging(); // These objects exposed here will become available to the JS code in our diff --git a/desktop/src/utils/error.ts b/desktop/src/utils/error.ts index 1922045a2..e69de29bb 100644 --- a/desktop/src/utils/error.ts +++ b/desktop/src/utils/error.ts @@ -1,17 +0,0 @@ -import { CustomErrors } from "../constants/errors"; - -export const isExecError = (err: any) => { - return err.message.includes("Command failed:"); -}; - -export const parseExecError = (err: any) => { - const errMessage = err.message; - if (errMessage.includes("Bad CPU type in executable")) { - return CustomErrors.UNSUPPORTED_PLATFORM( - process.platform, - process.arch, - ); - } else { - return errMessage; - } -};