Files
LTX-DeskWEB/shared/electron-api-schema.ts

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
}