From 3744d3448ed5e9c8de0e30ed28bb9107a5fffd9c Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Fri, 27 Mar 2026 20:18:05 +1100 Subject: [PATCH] feat(electron): persist recording session offsets and model checks --- electron/electron-env.d.ts | 9 +++++- electron/ipc/handlers.ts | 59 +++++++++++++++++++++++++++++++++----- electron/preload.ts | 3 ++ 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 7b8876e0..70944e35 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -153,6 +153,12 @@ interface Window { path?: string | null; error?: string; }>; + getWhisperModelPathStatus: (modelPath?: string | null) => Promise<{ + success: boolean; + exists: boolean; + path?: string | null; + error?: string; + }>; downloadWhisperSmallModel: () => Promise<{ success: boolean; path?: string; @@ -183,10 +189,11 @@ interface Window { setCurrentRecordingSession: (session: { videoPath: string; webcamPath?: string | null; + timeOffsetMs?: number; }) => Promise<{ success: boolean }>; getCurrentRecordingSession: () => Promise<{ success: boolean; - session?: { videoPath: string; webcamPath?: string | null }; + session?: { videoPath: string; webcamPath?: string | null; timeOffsetMs?: number }; }>; getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }>; clearCurrentVideoPath: () => Promise<{ success: boolean }>; diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index e239762e..09ea8970 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -81,12 +81,14 @@ type WindowBounds = { type RecordingSessionData = { videoPath: string webcamPath?: string | null + timeOffsetMs?: number } type RecordingSessionManifest = { - version: 1 + version: 1 | 2 videoFileName: string webcamFileName?: string | null + timeOffsetMs?: number } type ProjectLibraryEntry = { @@ -384,6 +386,7 @@ async function loadProjectFromPath(projectPath: string) { currentRecordingSession = { videoPath: mediaSources.videoPath, webcamPath: mediaSources.webcamPath, + timeOffsetMs: mediaSources.timeOffsetMs, } await rememberRecentProject(normalizedPath) @@ -420,6 +423,7 @@ async function resolveProjectMediaSources(project: unknown): Promise< success: true videoPath: string webcamPath: string | null + timeOffsetMs: number } | { success: false @@ -453,6 +457,9 @@ async function resolveProjectMediaSources(project: unknown): Promise< typeof (project as { editor?: { webcam?: { sourcePath?: unknown } } }).editor?.webcam?.sourcePath === 'string' ? ((project as { editor?: { webcam?: { sourcePath?: string } } }).editor?.webcam?.sourcePath ?? null) : null + const timeOffsetMs = normalizeRecordingTimeOffsetMs( + (project as { editor?: { webcam?: { timeOffsetMs?: unknown } } }).editor?.webcam?.timeOffsetMs, + ) const normalizedWebcamPath = normalizeVideoSourcePath(rawWebcamPath) if (!normalizedWebcamPath) { @@ -460,6 +467,7 @@ async function resolveProjectMediaSources(project: unknown): Promise< success: true, videoPath: normalizedVideoPath, webcamPath: null, + timeOffsetMs, } } @@ -469,16 +477,24 @@ async function resolveProjectMediaSources(project: unknown): Promise< success: true, videoPath: normalizedVideoPath, webcamPath: normalizedWebcamPath, + timeOffsetMs, } } catch { return { success: true, videoPath: normalizedVideoPath, webcamPath: null, + timeOffsetMs, } } } +function normalizeRecordingTimeOffsetMs(value: unknown): number { + return typeof value === 'number' && Number.isFinite(value) + ? Math.round(value) + : 0 +} + function getRecordingSessionManifestPath(videoPath: string) { const extension = path.extname(videoPath) const baseName = path.basename(videoPath, extension) @@ -500,9 +516,10 @@ async function persistRecordingSessionManifest(session: RecordingSessionData): P } const manifest: RecordingSessionManifest = { - version: 1, + version: 2, videoFileName: path.basename(normalizedVideoPath), webcamFileName: path.basename(normalizedWebcamPath), + timeOffsetMs: normalizeRecordingTimeOffsetMs(session.timeOffsetMs), } await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8') @@ -519,7 +536,7 @@ async function resolveRecordingSessionManifest(videoPath?: string | null): Promi try { const content = await fs.readFile(manifestPath, 'utf-8') const parsed = JSON.parse(content) as Partial - if (parsed.version !== 1) { + if (parsed.version !== 1 && parsed.version !== 2) { return null } @@ -531,6 +548,7 @@ async function resolveRecordingSessionManifest(videoPath?: string | null): Promi return { videoPath: normalizedVideoPath, webcamPath: null, + timeOffsetMs: 0, } } @@ -540,6 +558,7 @@ async function resolveRecordingSessionManifest(videoPath?: string | null): Promi return { videoPath: normalizedVideoPath, webcamPath, + timeOffsetMs: normalizeRecordingTimeOffsetMs(parsed.timeOffsetMs), } } catch { return null @@ -594,6 +613,7 @@ async function resolveRecordingSession(videoPath?: string | null): Promise { + try { + return await getWhisperModelPathStatus(modelPath) + } catch (error) { + return { success: false, exists: false, path: null, error: String(error) } + } + }) + ipcMain.handle('download-whisper-small-model', async (event) => { try { const existing = await getWhisperSmallModelStatus() @@ -4241,6 +4284,7 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh} ?? { videoPath: currentVideoPath, webcamPath: null, + timeOffsetMs: 0, } currentRecordingSession = resolvedSession @@ -4253,12 +4297,13 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh} return { success: true, webcamPath: resolvedSession.webcamPath ?? null } }) - ipcMain.handle('set-current-recording-session', async (_, session: { videoPath: string; webcamPath?: string | null }) => { + ipcMain.handle('set-current-recording-session', async (_, session: { videoPath: string; webcamPath?: string | null; timeOffsetMs?: number }) => { const normalizedVideoPath = normalizeVideoSourcePath(session.videoPath) ?? session.videoPath currentVideoPath = normalizedVideoPath currentRecordingSession = { videoPath: normalizedVideoPath, webcamPath: normalizeVideoSourcePath(session.webcamPath ?? null), + timeOffsetMs: normalizeRecordingTimeOffsetMs(session.timeOffsetMs), } currentProjectPath = null await persistRecordingSessionManifest(currentRecordingSession) diff --git a/electron/preload.ts b/electron/preload.ts index cfe9d8f4..80d1e424 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -167,6 +167,9 @@ contextBridge.exposeInMainWorld("electronAPI", { getWhisperSmallModelStatus: () => { return ipcRenderer.invoke("get-whisper-small-model-status"); }, + getWhisperModelPathStatus: (modelPath?: string | null) => { + return ipcRenderer.invoke("get-whisper-model-path-status", modelPath); + }, downloadWhisperSmallModel: () => { return ipcRenderer.invoke("download-whisper-small-model"); },