394 lines
10 KiB
TypeScript
394 lines
10 KiB
TypeScript
import { z } from 'zod'
|
|
|
|
const fileFilter = z.object({ name: z.string(), extensions: z.array(z.string()) })
|
|
|
|
function ipcResult<T extends z.ZodRawShape>(valueShape: T) {
|
|
return z.discriminatedUnion('success', [
|
|
z.object({ success: z.literal(true), ...valueShape }),
|
|
z.object({ success: z.literal(false), error: z.string() }),
|
|
])
|
|
}
|
|
|
|
export type IpcResult<T extends z.ZodRawShape> = z.infer<ReturnType<typeof ipcResult<T>>>
|
|
|
|
const emptyResult = ipcResult({})
|
|
|
|
const exportClip = z.object({
|
|
path: z.string(),
|
|
type: z.string(),
|
|
startTime: z.number(),
|
|
duration: z.number(),
|
|
trimStart: z.number(),
|
|
speed: z.number(),
|
|
reversed: z.boolean(),
|
|
flipH: z.boolean(),
|
|
flipV: z.boolean(),
|
|
opacity: z.number(),
|
|
trackIndex: z.number(),
|
|
muted: z.boolean(),
|
|
volume: z.number(),
|
|
})
|
|
|
|
const exportSubtitle = z.object({
|
|
text: z.string(),
|
|
startTime: z.number(),
|
|
endTime: z.number(),
|
|
style: z.object({
|
|
fontSize: z.number(),
|
|
fontFamily: z.string(),
|
|
fontWeight: z.string(),
|
|
color: z.string(),
|
|
backgroundColor: z.string(),
|
|
position: z.string(),
|
|
italic: z.boolean(),
|
|
}),
|
|
})
|
|
|
|
const logsResponse = z.object({
|
|
logPath: z.string(),
|
|
lines: z.array(z.string()),
|
|
error: z.string().optional(),
|
|
})
|
|
|
|
const backendHealthStatus = z.object({
|
|
status: z.enum(['alive', 'restarting', 'dead']),
|
|
exitCode: z.number().nullable().optional(),
|
|
})
|
|
|
|
export type BackendHealthStatus = z.infer<typeof backendHealthStatus>
|
|
|
|
const updateStatePayload = z.object({
|
|
status: z.enum(['idle', 'checking', 'available', 'downloading', 'downloaded', 'not-available']),
|
|
currentVersion: z.string(),
|
|
version: z.string().optional(),
|
|
releaseNotes: z.string().optional(),
|
|
percent: z.number().optional(),
|
|
message: z.string().optional(),
|
|
})
|
|
export type UpdateStatePayload = z.infer<typeof updateStatePayload>
|
|
|
|
export const electronAPISchemas = {
|
|
// App info
|
|
getBackend: {
|
|
input: z.object({}),
|
|
output: z.object({ url: z.string(), token: z.string() }),
|
|
},
|
|
getModelsPath: {
|
|
input: z.object({}),
|
|
output: z.string(),
|
|
},
|
|
readLocalFile: {
|
|
input: z.object({ filePath: z.string() }),
|
|
output: z.object({ data: z.string(), mimeType: z.string() }),
|
|
},
|
|
checkGpu: {
|
|
input: z.object({}),
|
|
output: z.object({ available: z.boolean(), name: z.string().optional(), vram: z.number().optional() }),
|
|
},
|
|
getAppInfo: {
|
|
input: z.object({}),
|
|
output: z.object({ version: z.string(), isPackaged: z.boolean(), modelsPath: z.string(), userDataPath: z.string() }),
|
|
},
|
|
|
|
// First-run setup
|
|
checkFirstRun: {
|
|
input: z.object({}),
|
|
output: z.object({ needsSetup: z.boolean(), needsLicense: z.boolean() }),
|
|
},
|
|
acceptLicense: {
|
|
input: z.object({}),
|
|
output: z.boolean(),
|
|
},
|
|
completeSetup: {
|
|
input: z.object({}),
|
|
output: z.boolean(),
|
|
},
|
|
fetchLicenseText: {
|
|
input: z.object({}),
|
|
output: z.string(),
|
|
},
|
|
getNoticesText: {
|
|
input: z.object({}),
|
|
output: z.string(),
|
|
},
|
|
|
|
// Open external pages / folders
|
|
openLtxApiKeyPage: {
|
|
input: z.object({}),
|
|
output: z.boolean(),
|
|
},
|
|
openLtxBillingPage: {
|
|
input: z.object({}),
|
|
output: z.boolean(),
|
|
},
|
|
openFalApiKeyPage: {
|
|
input: z.object({}),
|
|
output: z.boolean(),
|
|
},
|
|
openHuggingFaceRepo: {
|
|
input: z.object({ repoId: z.string() }),
|
|
output: z.boolean(),
|
|
},
|
|
openExternalUrl: {
|
|
input: z.object({ url: z.string() }),
|
|
output: z.boolean(),
|
|
},
|
|
openHuggingFaceAuth: {
|
|
input: z.object({
|
|
clientId: z.string(),
|
|
redirectUri: z.string(),
|
|
scope: z.string(),
|
|
state: z.string(),
|
|
codeChallenge: z.string(),
|
|
codeChallengeMethod: z.string(),
|
|
}),
|
|
output: z.boolean(),
|
|
},
|
|
openParentFolderOfFile: {
|
|
input: z.object({ filePath: z.string() }),
|
|
output: z.void(),
|
|
},
|
|
showItemInFolder: {
|
|
input: z.object({ filePath: z.string() }),
|
|
output: z.void(),
|
|
},
|
|
|
|
// Logs
|
|
getLogs: {
|
|
input: z.object({ query: z.string().optional() }),
|
|
output: logsResponse,
|
|
},
|
|
getLogPath: {
|
|
input: z.object({}),
|
|
output: z.object({ logPath: z.string(), logDir: z.string() }),
|
|
},
|
|
openLogFolder: {
|
|
input: z.object({}),
|
|
output: z.boolean(),
|
|
},
|
|
|
|
// Paths
|
|
getResourcePath: {
|
|
input: z.object({}),
|
|
output: z.string().nullable(),
|
|
},
|
|
getDownloadsPath: {
|
|
input: z.object({}),
|
|
output: z.string(),
|
|
},
|
|
getFreeDiskSpace: {
|
|
input: z.object({ path: z.string() }),
|
|
output: ipcResult({ bytes: z.number() }),
|
|
},
|
|
|
|
// Project assets
|
|
addVisualAssetToProject: {
|
|
input: z.object({ srcPath: z.string(), projectId: z.string(), type: z.enum(['video', 'image']) }),
|
|
output: ipcResult({
|
|
path: z.string(),
|
|
bigThumbnailPath: z.string(),
|
|
smallThumbnailPath: z.string(),
|
|
width: z.number(),
|
|
height: z.number(),
|
|
}),
|
|
},
|
|
addGenericAssetToProject: {
|
|
input: z.object({ srcPath: z.string(), projectId: z.string() }),
|
|
output: ipcResult({ path: z.string() }),
|
|
},
|
|
prepareGenerationInput: {
|
|
input: z.object({ path: z.string() }),
|
|
output: ipcResult({ path: z.string() }),
|
|
},
|
|
makeThumbnailsForProjectAsset: {
|
|
input: z.object({ path: z.string(), type: z.enum(['video', 'image']) }),
|
|
output: ipcResult({
|
|
bigThumbnailPath: z.string(),
|
|
smallThumbnailPath: z.string(),
|
|
}),
|
|
},
|
|
makeDimensionsForProjectAsset: {
|
|
input: z.object({ path: z.string(), type: z.enum(['video', 'image']) }),
|
|
output: ipcResult({
|
|
width: z.number(),
|
|
height: z.number(),
|
|
}),
|
|
},
|
|
getProjectAssetsPath: {
|
|
input: z.object({}),
|
|
output: z.string(),
|
|
},
|
|
openProjectAssetsPathChangeDialog: {
|
|
input: z.object({}),
|
|
output: ipcResult({ path: z.string() }),
|
|
},
|
|
|
|
// File dialogs & save
|
|
showSaveDialog: {
|
|
input: z.object({
|
|
title: z.string().optional(),
|
|
defaultPath: z.string().optional(),
|
|
filters: z.array(fileFilter).optional(),
|
|
}),
|
|
output: z.string().nullable(),
|
|
},
|
|
saveFile: {
|
|
input: z.object({ filePath: z.string(), data: z.string(), encoding: z.string().optional() }),
|
|
output: ipcResult({ path: z.string() }),
|
|
},
|
|
saveBinaryFile: {
|
|
input: z.object({ filePath: z.string(), data: z.instanceof(ArrayBuffer) }),
|
|
output: ipcResult({ path: z.string() }),
|
|
},
|
|
showOpenDirectoryDialog: {
|
|
input: z.object({ title: z.string().optional() }),
|
|
output: z.string().nullable(),
|
|
},
|
|
searchDirectoryForFiles: {
|
|
input: z.object({ directory: z.string(), filenames: z.array(z.string()) }),
|
|
output: z.record(z.string(), z.string()),
|
|
},
|
|
checkFilesExist: {
|
|
input: z.object({ filePaths: z.array(z.string()) }),
|
|
output: z.record(z.string(), z.boolean()),
|
|
},
|
|
showOpenFileDialog: {
|
|
input: z.object({
|
|
title: z.string().optional(),
|
|
filters: z.array(fileFilter).optional(),
|
|
properties: z.array(z.string()).optional(),
|
|
}),
|
|
output: z.array(z.string()).nullable(),
|
|
},
|
|
|
|
// Video export
|
|
exportNative: {
|
|
input: z.object({
|
|
clips: z.array(exportClip),
|
|
outputPath: z.string(),
|
|
codec: z.string(),
|
|
width: z.number(),
|
|
height: z.number(),
|
|
fps: z.number(),
|
|
quality: z.number(),
|
|
letterbox: z.object({ ratio: z.number(), color: z.string(), opacity: z.number() }).optional(),
|
|
subtitles: z.array(exportSubtitle).optional(),
|
|
}),
|
|
output: emptyResult,
|
|
},
|
|
exportCancel: {
|
|
input: z.object({ sessionId: z.string() }),
|
|
output: emptyResult,
|
|
},
|
|
|
|
// Python setup
|
|
checkPythonReady: {
|
|
input: z.object({}),
|
|
output: z.object({ ready: z.boolean() }),
|
|
},
|
|
startPythonSetup: {
|
|
input: z.object({}),
|
|
output: z.void(),
|
|
},
|
|
startPythonBackend: {
|
|
input: z.object({}),
|
|
output: z.void(),
|
|
},
|
|
getBackendHealthStatus: {
|
|
input: z.object({}),
|
|
output: backendHealthStatus.nullable(),
|
|
},
|
|
// Tells the liveness monitor a generation is known to be in flight, so it doesn't mistake a
|
|
// long-running local generation (MPS/CUDA compute can starve the backend's own event loop for
|
|
// tens of seconds, delaying /health) for a genuinely hung process and kill it mid-generation.
|
|
notifyGenerationActive: {
|
|
input: z.object({ active: z.boolean() }),
|
|
output: z.void(),
|
|
},
|
|
|
|
// Video processing
|
|
extractVideoFrame: {
|
|
input: z.object({ videoPath: z.string(), seekTime: z.number(), width: z.number().optional(), quality: z.number().optional() }),
|
|
output: z.object({ path: z.string() }),
|
|
},
|
|
|
|
// Logging
|
|
writeLog: {
|
|
input: z.object({ level: z.string(), message: z.string() }),
|
|
output: z.void(),
|
|
},
|
|
|
|
// Models
|
|
openModelsDirChangeDialog: {
|
|
input: z.object({}),
|
|
output: ipcResult({ path: z.string() }),
|
|
},
|
|
openModelsFolder: {
|
|
// No path argument by design — the main process resolves the configured models dir
|
|
// from the backend so a renderer can't ask to open an arbitrary location.
|
|
input: z.object({}),
|
|
output: ipcResult({}),
|
|
},
|
|
|
|
// Analytics
|
|
getAnalyticsState: {
|
|
input: z.object({}),
|
|
output: z.object({ analyticsEnabled: z.boolean(), installationId: z.string() }),
|
|
},
|
|
setAnalyticsEnabled: {
|
|
input: z.object({ enabled: z.boolean() }),
|
|
output: z.void(),
|
|
},
|
|
sendAnalyticsEvent: {
|
|
input: z.object({ eventName: z.string(), extraDetails: z.record(z.string(), z.unknown()).nullable().optional() }),
|
|
output: z.void(),
|
|
},
|
|
|
|
// --- App updates ---
|
|
getUpdateState: {
|
|
input: z.object({}),
|
|
output: updateStatePayload,
|
|
},
|
|
checkForUpdatesNow: {
|
|
input: z.object({}),
|
|
output: emptyResult,
|
|
},
|
|
startUpdateDownload: {
|
|
input: z.object({}),
|
|
output: emptyResult,
|
|
},
|
|
installUpdateAndRestart: {
|
|
input: z.object({}),
|
|
output: emptyResult,
|
|
},
|
|
skipUpdateVersion: {
|
|
input: z.object({ version: z.string() }),
|
|
output: emptyResult,
|
|
},
|
|
getAutoCheckUpdates: {
|
|
input: z.object({}),
|
|
output: z.object({ enabled: z.boolean() }),
|
|
},
|
|
setAutoCheckUpdates: {
|
|
input: z.object({ enabled: z.boolean() }),
|
|
output: emptyResult,
|
|
},
|
|
} as const
|
|
|
|
type Schemas = typeof electronAPISchemas
|
|
|
|
type InvokeAPI = {
|
|
[K in keyof Schemas]: z.infer<Schemas[K]['input']> extends Record<string, never>
|
|
? () => Promise<z.infer<Schemas[K]['output']>>
|
|
: (input: z.infer<Schemas[K]['input']>) => Promise<z.infer<Schemas[K]['output']>>
|
|
}
|
|
|
|
export type ElectronAPI = InvokeAPI & {
|
|
onPythonSetupProgress: (cb: (data: unknown) => void) => void
|
|
removePythonSetupProgress: () => void
|
|
onBackendHealthStatus: (cb: (data: BackendHealthStatus) => void) => (() => void)
|
|
onUpdateEvent: (cb: (data: UpdateStatePayload) => void) => (() => void)
|
|
getPathForFile: (file: File) => string
|
|
platform: string
|
|
}
|