Create standalone browser port of LTX Athena with authenticated native API transport
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user