-
-
Notifications
You must be signed in to change notification settings - Fork 803
feat: add blur annotation and Whisper model selection for autocaption #116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
7d7b61b
ced02c9
7e8a40f
01350ad
e10270f
281a73b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,9 +30,48 @@ const AUTO_RECORDING_RETENTION_COUNT = 20 | |
| const AUTO_RECORDING_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000 | ||
| const ALLOW_RECORDLY_WINDOW_CAPTURE = Boolean(process.env['VITE_DEV_SERVER_URL']) | ||
| const RECORDING_SESSION_MANIFEST_SUFFIX = '.recordly-session.json' | ||
| const WHISPER_MODEL_DOWNLOAD_URL = 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.bin' | ||
| const WHISPER_MODEL_DIR = path.join(app.getPath('userData'), 'whisper') | ||
| const WHISPER_SMALL_MODEL_PATH = path.join(WHISPER_MODEL_DIR, 'ggml-small.bin') | ||
| const WHISPER_MODEL_DIR = path.join(app.getPath("userData"), "whisper"); | ||
|
|
||
| const WHISPER_MODELS = { | ||
| tiny: { | ||
| label: "Tiny", | ||
| size: "75 MB", | ||
| filename: "ggml-tiny.bin", | ||
| url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin", | ||
| }, | ||
| base: { | ||
| label: "Base", | ||
| size: "142 MB", | ||
| filename: "ggml-base.bin", | ||
| url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.bin", | ||
| }, | ||
| small: { | ||
| label: "Small", | ||
| size: "466 MB", | ||
| filename: "ggml-small.bin", | ||
| url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.bin", | ||
| }, | ||
| medium: { | ||
| label: "Medium", | ||
| size: "1.5 GB", | ||
| filename: "ggml-medium.bin", | ||
| url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-medium.bin", | ||
| }, | ||
| large: { | ||
| label: "Large (v3)", | ||
| size: "2.9 GB", | ||
| filename: "ggml-large-v3.bin", | ||
| url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3.bin", | ||
| }, | ||
| } as const; | ||
|
|
||
| function getWhisperModelPath(modelName: string) { | ||
| const model = WHISPER_MODELS[modelName as keyof typeof WHISPER_MODELS]; | ||
| if (!model) { | ||
| throw new Error(`Unsupported Whisper model: ${modelName}`); | ||
| } | ||
| return path.join(WHISPER_MODEL_DIR, model.filename); | ||
| } | ||
|
|
||
| function getAssetRootPath() { | ||
| if (app.isPackaged) { | ||
|
|
@@ -961,27 +1000,34 @@ function getFfmpegBinaryPath() { | |
| } | ||
|
|
||
| function sendWhisperModelDownloadProgress( | ||
| webContents: Electron.WebContents, | ||
| payload: { status: 'idle' | 'downloading' | 'downloaded' | 'error'; progress: number; path?: string | null; error?: string }, | ||
| webContents: Electron.WebContents, | ||
| payload: { | ||
| status: "idle" | "downloading" | "downloaded" | "error"; | ||
| progress: number; | ||
| model: string; | ||
| path?: string | null; | ||
| error?: string; | ||
| }, | ||
| ) { | ||
| webContents.send('whisper-small-model-download-progress', payload) | ||
| } | ||
|
|
||
| async function getWhisperSmallModelStatus() { | ||
| try { | ||
| await fs.access(WHISPER_SMALL_MODEL_PATH, fsConstants.R_OK) | ||
| return { | ||
| success: true, | ||
| exists: true, | ||
| path: WHISPER_SMALL_MODEL_PATH, | ||
| } | ||
| } catch { | ||
| return { | ||
| success: true, | ||
| exists: false, | ||
| path: null, | ||
| } | ||
| } | ||
| webContents.send("whisper-model-download-progress", payload); | ||
| } | ||
|
|
||
| async function getWhisperModelStatus(_event: any, modelName: string) { | ||
| try { | ||
| const modelPath = getWhisperModelPath(modelName); | ||
| await fs.access(modelPath, fsConstants.R_OK); | ||
| return { | ||
| success: true, | ||
| exists: true, | ||
| path: modelPath, | ||
| }; | ||
| } catch { | ||
| return { | ||
| success: true, | ||
| exists: false, | ||
| path: null, | ||
| }; | ||
| } | ||
|
Comment on lines
+1050
to
+1065
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't swallow unsupported models as “not downloaded.” This helper currently converts every failure into 🧰 Tools🪛 Biome (2.4.9)[error] 1052-1052: Unexpected any. Specify a different type. (lint/suspicious/noExplicitAny) 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| function downloadFileWithProgress( | ||
|
|
@@ -1048,46 +1094,60 @@ function downloadFileWithProgress( | |
| return request(url) | ||
| } | ||
|
|
||
| async function downloadWhisperSmallModel(webContents: Electron.WebContents) { | ||
| await fs.mkdir(WHISPER_MODEL_DIR, { recursive: true }) | ||
| const tempPath = `${WHISPER_SMALL_MODEL_PATH}.download` | ||
|
|
||
| sendWhisperModelDownloadProgress(webContents, { | ||
| status: 'downloading', | ||
| progress: 0, | ||
| path: null, | ||
| }) | ||
|
|
||
| try { | ||
| await fs.rm(tempPath, { force: true }) | ||
| await downloadFileWithProgress(WHISPER_MODEL_DOWNLOAD_URL, tempPath, (progress) => { | ||
| sendWhisperModelDownloadProgress(webContents, { | ||
| status: 'downloading', | ||
| progress, | ||
| path: null, | ||
| }) | ||
| }) | ||
| await fs.rename(tempPath, WHISPER_SMALL_MODEL_PATH) | ||
| sendWhisperModelDownloadProgress(webContents, { | ||
| status: 'downloaded', | ||
| progress: 100, | ||
| path: WHISPER_SMALL_MODEL_PATH, | ||
| }) | ||
| return WHISPER_SMALL_MODEL_PATH | ||
| } catch (error) { | ||
| await fs.rm(tempPath, { force: true }).catch(() => undefined) | ||
| sendWhisperModelDownloadProgress(webContents, { | ||
| status: 'error', | ||
| progress: 0, | ||
| path: null, | ||
| error: String(error), | ||
| }) | ||
| throw error | ||
| } | ||
| } | ||
|
|
||
| async function deleteWhisperSmallModel() { | ||
| await fs.rm(WHISPER_SMALL_MODEL_PATH, { force: true }) | ||
| async function downloadWhisperModel( | ||
| webContents: Electron.WebContents, | ||
| modelName: string, | ||
| ) { | ||
| const model = WHISPER_MODELS[modelName as keyof typeof WHISPER_MODELS]; | ||
| if (!model) { | ||
| throw new Error(`Unsupported Whisper model: ${modelName}`); | ||
| } | ||
|
|
||
| await fs.mkdir(WHISPER_MODEL_DIR, { recursive: true }); | ||
| const modelPath = getWhisperModelPath(modelName); | ||
| const tempPath = `${modelPath}.download`; | ||
|
|
||
| sendWhisperModelDownloadProgress(webContents, { | ||
| status: "downloading", | ||
| progress: 0, | ||
| model: modelName, | ||
| path: null, | ||
| }); | ||
|
|
||
| try { | ||
| await fs.rm(tempPath, { force: true }); | ||
| await downloadFileWithProgress(model.url, tempPath, (progress) => { | ||
| sendWhisperModelDownloadProgress(webContents, { | ||
| status: "downloading", | ||
| progress, | ||
| model: modelName, | ||
| path: null, | ||
| }); | ||
| }); | ||
| await fs.rename(tempPath, modelPath); | ||
| sendWhisperModelDownloadProgress(webContents, { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's locate the file and examine the context around lines 1154-1165
fd handlers.ts | head -5Repository: webadderall/Recordly Length of output: 152 🏁 Script executed: # Read the relevant section of the file
wc -l electron/ipc/handlers.tsRepository: webadderall/Recordly Length of output: 93 🏁 Script executed: # Get the context around the suspicious code
sed -n '1140,1180p' electron/ipc/handlers.ts | cat -nRepository: webadderall/Recordly Length of output: 1396 🏁 Script executed: # Find the downloadFileWithProgress function definition
rg -n "downloadFileWithProgress" --type tsRepository: webadderall/Recordly Length of output: 228 🏁 Script executed: # Get the downloadFileWithProgress function starting at line 1070
sed -n '1070,1120p' electron/ipc/handlers.ts | cat -nRepository: webadderall/Recordly Length of output: 2187 🏁 Script executed: # Get the rest of the function
sed -n '1120,1135p' electron/ipc/handlers.ts | cat -nRepository: webadderall/Recordly Length of output: 417 Wait for the file handle to close before renaming. The 🤖 Prompt for AI Agents |
||
| status: "downloaded", | ||
| progress: 100, | ||
| model: modelName, | ||
| path: modelPath, | ||
| }); | ||
| return modelPath; | ||
| } catch (error) { | ||
| await fs.rm(tempPath, { force: true }).catch(() => undefined); | ||
| sendWhisperModelDownloadProgress(webContents, { | ||
| status: "error", | ||
| progress: 0, | ||
| model: modelName, | ||
| path: null, | ||
| error: String(error), | ||
| }); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| async function deleteWhisperModel(_event: any, modelName: string) { | ||
| const modelPath = getWhisperModelPath(modelName); | ||
| await fs.rm(modelPath, { force: true }); | ||
| } | ||
|
|
||
| function parseSrtTimestamp(value: string) { | ||
|
|
@@ -3975,45 +4035,47 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh} | |
| } | ||
| }) | ||
|
|
||
| ipcMain.handle('get-whisper-small-model-status', async () => { | ||
| ipcMain.handle('get-whisper-model-status', async (event, modelName: string) => { | ||
| try { | ||
| return await getWhisperSmallModelStatus() | ||
| return await getWhisperModelStatus(event, modelName) | ||
| } catch (error) { | ||
| return { success: false, exists: false, path: null, error: String(error) } | ||
| } | ||
| }) | ||
|
|
||
| ipcMain.handle('download-whisper-small-model', async (event) => { | ||
| ipcMain.handle('download-whisper-model', async (event, modelName: string) => { | ||
| try { | ||
| const existing = await getWhisperSmallModelStatus() | ||
| const existing = await getWhisperModelStatus(event, modelName) | ||
| if (existing.exists) { | ||
| sendWhisperModelDownloadProgress(event.sender, { | ||
| status: 'downloaded', | ||
| progress: 100, | ||
| model: modelName, | ||
| path: existing.path, | ||
| }) | ||
| return { success: true, path: existing.path, alreadyDownloaded: true } | ||
| } | ||
|
|
||
| const modelPath = await downloadWhisperSmallModel(event.sender) | ||
| const modelPath = await downloadWhisperModel(event.sender, modelName) | ||
| return { success: true, path: modelPath } | ||
| } catch (error) { | ||
| console.error('Failed to download Whisper small model:', error) | ||
| console.error(`Failed to download Whisper model ${modelName}:`, error) | ||
| return { success: false, error: String(error) } | ||
| } | ||
| }) | ||
|
|
||
| ipcMain.handle('delete-whisper-small-model', async (event) => { | ||
| ipcMain.handle('delete-whisper-model', async (event, modelName: string) => { | ||
| try { | ||
| await deleteWhisperSmallModel() | ||
| await deleteWhisperModel(event, modelName) | ||
| sendWhisperModelDownloadProgress(event.sender, { | ||
| status: 'idle', | ||
| progress: 0, | ||
| model: modelName, | ||
| path: null, | ||
| }) | ||
|
Comment on lines
+4168
to
4176
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
rg -n -C2 'delete-whisper-model'
rg -n -C2 'generate-auto-captions|whisperModelPath'Repository: webadderall/Recordly Length of output: 13489 🏁 Script executed: # Find the deleteWhisperModel function implementation
rg -n 'deleteWhisperModel' electron/ipc/handlers.ts -A 20 | head -60
# Find whisper model resolution logic
rg -n 'resolveWhisperModel|Unsupported Whisper model'
# Check if there's model name to path resolution
rg -n 'whisperSelectedModel|selectedModel' electron/ipc/handlers.ts | head -20Repository: webadderall/Recordly Length of output: 1597 🏁 Script executed: # Find getWhisperModelPath implementation
rg -n 'getWhisperModelPath' -A 30
# Check the error context at lines 71 and 1140
sed -n '65,80p' electron/ipc/handlers.ts
sed -n '1135,1150p' electron/ipc/handlers.ts
# Find selectedModel usage to understand model selection flow
rg -n 'whisperSelectedModel' electron/ipc/handlers.ts -B 3 -A 3Repository: webadderall/Recordly Length of output: 7745 🏁 Script executed: # View the full generate-auto-captions handler starting at line 4213
sed -n '4213,4280p' electron/ipc/handlers.tsRepository: webadderall/Recordly Length of output: 2603 🏁 Script executed: # Find generateAutoCaptionsFromVideo function implementation
rg -n 'generateAutoCaptionsFromVideo' -A 50 | head -80Repository: webadderall/Recordly Length of output: 5601 🏁 Script executed: # Check how the renderer calls these IPC handlers
rg -n 'generateAutoCaptions|deleteWhisperModel' src/components/video-editor/ -B 2 -A 2 | head -100
# Also check what happens when whisperModelPath is null/undefined
rg -n 'whisperModelPath.*null|whisperModelPath.*undefined' src/components/video-editor/VideoEditor.tsx -B 2 -A 2Repository: webadderall/Recordly Length of output: 1566 🏁 Script executed: # View the full generateAutoCaptions call context
sed -n '1605,1620p' src/components/video-editor/VideoEditor.tsx
# Check if there are other places calling delete-whisper-model
rg -n 'deleteWhisperModel' src/components/ -B 3 -A 3
# Check what autoCaptionSettings.selectedModel is and if it could be undefined
rg -n 'autoCaptionSettings.*selectedModel|selectedModel.*=' src/components/video-editor/VideoEditor.tsx -B 2 -A 2 | head -40Repository: webadderall/Recordly Length of output: 2285 🏁 Script executed: # Check the autoCaptionSettings structure and initialization
rg -n 'autoCaptionSettings.*=' src/components/video-editor/VideoEditor.tsx -B 1 -A 1 | head -40
# Check the default value for selectedModel
rg -n 'selectedModel.*=' src/components/video-editor/ -B 2 -A 2 | head -50
# Look at the flow when model download completes - line 1425
sed -n '1415,1445p' src/components/video-editor/VideoEditor.tsxRepository: webadderall/Recordly Length of output: 3335 Unify model identity across the IPC boundary.
🤖 Prompt for AI Agents |
||
| return { success: true } | ||
| } catch (error) { | ||
| console.error('Failed to delete Whisper small model:', error) | ||
| console.error(`Failed to delete Whisper model ${modelName}:`, error) | ||
| return { success: false, error: String(error) } | ||
| } | ||
| }) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "version": "v1.8.4", | ||
| "platform": "win32", | ||
| "arch": "x64", | ||
| "binary": "whisper-cli.exe" | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 80
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 687
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 1251
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 2496
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 610
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 1557
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 1813
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 1180
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 351
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 1197
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 1948
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 1383
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 824
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 46
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 2957
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 757
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 252
🌐 Web query:
Electron WebContents.send() on destroyed window throws error or fails silently💡 Result:
Yes, calling webContents.send on a destroyed WebContents throws an "Object has been destroyed" error. This is a well-documented behavior in Electron across multiple versions. To prevent the error, always check webContents.isDestroyed before calling send: if (!win.webContents.isDestroyed) { win.webContents.send('channel', data); } This check is explicitly recommended in the official Electron documentation, which provides the isDestroyed method to determine if the web page is destroyed. Multiple GitHub issues and Stack Overflow reports confirm that attempting to send IPC messages after destruction (e.g., after window close) triggers this error, and the standard solution is the isDestroyed check. The official docs do not indicate that send fails silently; it throws an error, though some related serialization errors may log to console without throwing in specific cases (unrelated to destruction).
Citations:
Guard
webContents.send()calls in progress callbacks to prevent main-process crashes.These functions call
webContents.send()from asynchronous progress callbacks without checking if the WebContents is destroyed. If the user closes the window during a long download or caption operation, Electron will throw "Object has been destroyed" when the callback fires, crashing the main process if the exception is unhandled. Add!webContents.isDestroyed()checks before each send call. The codebase already uses this pattern elsewhere (see lines 90, 1891, 2165, 2209); apply the same guard here.Locations:
sendWhisperModelDownloadProgress()helper functiondownloadWhisperModel()generateAutoCaptionsFromVideo()🤖 Prompt for AI Agents