Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
586b979
fix: adjust margin-top in FileQueue CSS for better layout
pedrovsiqueira Jan 15, 2026
d6b7302
fix: simplify language argument handling in transcribe function
pedrovsiqueira Feb 19, 2026
f6746dd
feat: implement duplicate file handling in batch queue and UI updates
pedrovsiqueira Feb 20, 2026
5011ecc
feat: add retry functionality for failed and cancelled transcription …
pedrovsiqueira Feb 20, 2026
ca1973b
feat: implement showItemInFolder functionality and related tests
pedrovsiqueira Feb 20, 2026
51d51ad
feat: add estimated time remaining feature to file queue and batch pr…
pedrovsiqueira Feb 20, 2026
854be2c
feat: estimate remaining time during transcription processing
pedrovsiqueira Feb 20, 2026
e5df5ca
feat: add queue resume functionality with UI prompt and local storage…
pedrovsiqueira Feb 20, 2026
a3e6735
feat: add search functionality to transcription history with filterin…
pedrovsiqueira Feb 20, 2026
f02eb73
feat: update README and documentation to reflect new features
pedrovsiqueira Feb 20, 2026
f4b332c
feat: add support for additional audio formats (OPUS, OGA, AMR) and u…
pedrovsiqueira Feb 20, 2026
afe0a64
fix: resolve file path validation logic and enhance language auto-det…
pedrovsiqueira Feb 20, 2026
35c312c
fix: improve error handling and resource management in file fingerpri…
pedrovsiqueira Feb 20, 2026
4a66496
test: add test for clearing duplicate skipped badge count when queue …
pedrovsiqueira Feb 20, 2026
f452caf
refactor: update file handling to use async/await and improve queue p…
pedrovsiqueira Feb 20, 2026
b02add8
test: ensure model select is enabled before changing selection in Set…
pedrovsiqueira Feb 20, 2026
0466600
feat: implement safeSend utility for IPC communication and enhance er…
pedrovsiqueira Feb 20, 2026
5143d07
feat: enhance FileQueue with user-friendly error messages and toast n…
pedrovsiqueira Feb 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,21 @@ A beautiful, native macOS desktop application for transcribing audio and video f

- **Drag & Drop** - Drag single or multiple files to create a batch queue
- **Batch Processing** - Process unlimited files sequentially with automatic queue management
- **Multiple Formats** - Supports MP3, WAV, M4A, FLAC, OGG, WMA, AAC, AIFF, MP4, MOV, AVI, MKV, WebM, WMV, FLV, M4V
- **Queue Persistence + Resume** - Restore unfinished queue items after restarting the app
- **Duplicate File Protection** - Automatically skips duplicates by file path/fingerprint in batch mode
- **Retry Failed Items** - One-click retry for failed or cancelled queue items
- **Live Batch ETA** - See estimated remaining time while batch processing is running
- **Completion Notifications** - Native notification when a batch finishes
- **Multiple Formats** - Supports MP3, WAV, M4A, FLAC, OGG, OPUS, OGA, AMR, WMA, AAC, AIFF, MP4, MOV, AVI, MKV, WebM, WMV, FLV, M4V
- **Multiple Models** - Choose from tiny, base, small, medium, large-v3, or large-v3-turbo Whisper models (including English-only variants)
- **Output Formats** - Export as VTT subtitles, SRT subtitles, plain text, Word (`.docx`), PDF, or Markdown
- **Finder Reveal After Save** - Prompt to reveal the saved transcript directly in Finder
- **Language Support** - Auto-detect or select from 12+ languages
- **Apple Silicon Optimized** - Native Metal GPU acceleration on M1/M2/M3/M4 Macs
- **Dark Mode** - Beautiful dark theme that respects your system preference
- **Auto Updates** - Automatic update notifications when new versions are available
- **Keyboard Shortcuts** - Full keyboard navigation support
- **Transcription History** - Keep track of your recent transcriptions
- **Transcription History + Search** - Keep track of recent transcriptions and search by file name, content, model, or language
- **Native Performance** - Uses whisper.cpp for fast, efficient transcription
- **TypeScript** - Fully typed codebase for better maintainability
- **Feature-Driven Architecture** - Modular codebase organized by feature domains
Expand Down
36 changes: 24 additions & 12 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
<meta name="title"
content="WhisperDesk - Free Local AI Audio Transcription for macOS | Privacy-First Speech-to-Text">
<meta name="description"
content="Free, open-source audio transcription app for macOS. Transcribe audio & video files privately using OpenAI's Whisper AI locally. No cloud uploads, no subscriptions. Supports Apple Silicon & Intel. Export to SRT, VTT, TXT.">
content="Free, open-source audio transcription app for macOS. Transcribe audio & video files privately using OpenAI's Whisper AI locally, including OPUS/OGA/AMR voice notes. No cloud uploads, no subscriptions. Supports Apple Silicon & Intel. Export to SRT, VTT, TXT, DOCX, PDF, and Markdown. Smart batch queue with resume, retry, live ETA, duplicate skipping, and searchable history">
<meta name="keywords"
content="whisper, transcription, audio transcription, video transcription, speech to text, voice to text, AI transcription, local transcription, offline transcription, macOS transcription app, free transcription software, open source transcription, whisper.cpp, OpenAI Whisper, Apple Silicon, M1, M2, M3, M4, subtitle generator, SRT generator, VTT generator, podcast transcription, meeting transcription, interview transcription, private transcription, secure transcription">
content="whisper, transcription, audio transcription, video transcription, speech to text, voice to text, AI transcription, local transcription, offline transcription, macOS transcription app, free transcription software, open source transcription, whisper.cpp, OpenAI Whisper, Apple Silicon, M1, M2, M3, M4, subtitle generator, SRT generator, VTT generator, podcast transcription, meeting transcription, interview transcription, private transcription, secure transcription, opus transcription, oga transcription, amr transcription, whatsapp voice note transcription">
<meta name="author" content="PVAS Development">
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
<meta name="googlebot" content="index, follow">
Expand Down Expand Up @@ -61,13 +61,16 @@
},
"license": "https://opensource.org/licenses/MIT",
"screenshot": "https://whisperdesk.org/screenshot.png",
"softwareVersion": "1.1.6",
"softwareVersion": "1.9.0",
"downloadUrl": "https://github.com/PVAS-Development/whisperdesk/releases/latest",
"featureList": [
"100% Local Processing",
"GPU Accelerated (Apple Silicon Metal)",
"Multi-format Export (SRT, VTT, JSON, TXT)",
"Batch Processing Queue",
"Batch Queue with ETA, Retry, and Duplicate Protection",
"Queue Persistence and Resume After Restart",
"Transcription History Search",
"Multi-format Export (VTT, SRT, TXT, DOCX, PDF, Markdown)",
"Finder Reveal After Save",
"No Internet Required",
"No Subscription Fees",
"Open Source (MIT License)"
Expand Down Expand Up @@ -134,7 +137,7 @@
"name": "What formats can WhisperDesk export?",
"acceptedAnswer": {
"@type": "Answer",
"text": "WhisperDesk supports exporting transcriptions to SRT (subtitles), VTT (web subtitles), JSON, and plain text formats."
"text": "WhisperDesk can export transcripts as VTT, SRT, TXT, Markdown, DOCX, and PDF."
}
},
{
Expand All @@ -144,6 +147,14 @@
"@type": "Answer",
"text": "Yes, WhisperDesk is optimized for Apple Silicon (M1, M2, M3, M4) Macs with Metal GPU acceleration for blazing fast transcription speeds up to 100x real-time."
}
},
{
"@type": "Question",
"name": "Can I resume my transcription queue after restarting the app?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. WhisperDesk restores unfinished queue items after restart and lets you resume processing with one click."
}
}
]
}
Expand Down Expand Up @@ -175,8 +186,8 @@
<div class="nav-links">
<a href="#features" class="nav-link">Features</a>
<a href="#how-it-works" class="nav-link">How it Works</a>
<a href="https://www.paypal.com/donate/?hosted_button_id=HTJXGMEGMWWD6" class="nav-link-icon" target="_blank"
rel="noopener noreferrer" aria-label="Donate to WhisperDesk">
<a href="https://www.paypal.com/donate/?hosted_button_id=HTJXGMEGMWWD6" class="nav-link-icon"
target="_blank" rel="noopener noreferrer" aria-label="Donate to WhisperDesk">
<i data-lucide="heart" class="heart-icon"></i>
</a>
<a href="https://github.com/PVAS-Development/whisperdesk" class="nav-link-icon" target="_blank"
Expand Down Expand Up @@ -286,7 +297,8 @@ <h3 class="feature-title">Multi-Format Export</h3>
</div>
<h3 class="feature-title">Batch Processing</h3>
<p class="feature-description">
Drag and drop multiple files or folders. Process unlimited audio files sequentially with automatic queue management.
Drag and drop multiple files or folders. Process unlimited audio files sequentially with
automatic queue management.
</p>
</article>
</div>
Expand Down Expand Up @@ -343,8 +355,8 @@ <h2 id="opensource-title" class="opensource-title">Open Source & Community Drive
<nav class="footer-links" aria-label="Footer navigation">
<a href="https://www.paypal.com/donate/?hosted_button_id=HTJXGMEGMWWD6" target="_blank"
rel="noopener noreferrer" class="footer-link">Donate</a>
<a href="https://www.buymeacoffee.com/pedrovsiqueira" target="_blank"
rel="noopener noreferrer" class="footer-link">Buy me a coffee</a>
<a href="https://www.buymeacoffee.com/pedrovsiqueira" target="_blank" rel="noopener noreferrer"
class="footer-link">Buy me a coffee</a>
<a href="https://github.com/PVAS-Development/whisperdesk/blob/main/LICENSE" target="_blank"
rel="noopener noreferrer" class="footer-link">License</a>
<a href="https://github.com/PVAS-Development/whisperdesk/blob/main/CONTRIBUTING.md" target="_blank"
Expand Down Expand Up @@ -411,4 +423,4 @@ <h2 id="opensource-title" class="opensource-title">Open Source & Community Drive
</script>
</body>

</html>
</html>
30 changes: 24 additions & 6 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import path from 'path';
import { registerIpcHandlers } from './ipc';
import { initAnalytics, trackEvent, AnalyticsEvents } from './services/analytics';
import { initAutoUpdater, checkForUpdates } from './services/auto-updater';
import { safeSend } from './utils/safe-send';
import packageJson from '../../package.json';

initAnalytics();

const APP_DISPLAY_NAME = 'WhisperDesk';
const APP_USER_MODEL_ID = 'com.whisperdesk.app';

let mainWindow: BrowserWindow | null = null;
let ipcHandlersRegistered = false;

Expand All @@ -16,6 +20,19 @@ const appVersion = packageJson.version;

const UPDATE_CHECK_DELAY_MS = 3000;

app.setName(APP_DISPLAY_NAME);

if (process.platform === 'win32') {
app.setAppUserModelId(APP_USER_MODEL_ID);
}

if (process.platform === 'darwin') {
app.setAboutPanelOptions({
applicationName: APP_DISPLAY_NAME,
applicationVersion: appVersion,
});
}

function createMenu() {
if (!mainWindow) return;

Expand All @@ -27,14 +44,14 @@ function createMenu() {
label: 'Open File...',
accelerator: 'CmdOrCtrl+O',
click: () => {
mainWindow?.webContents.send('menu:openFile');
safeSend(mainWindow, 'menu:openFile');
},
},
{
label: 'Save Transcription...',
accelerator: 'CmdOrCtrl+S',
click: () => {
mainWindow?.webContents.send('menu:saveFile');
safeSend(mainWindow, 'menu:saveFile');
},
},
{ type: 'separator' as const },
Expand All @@ -60,7 +77,7 @@ function createMenu() {
label: 'Copy All Transcription',
accelerator: 'CmdOrCtrl+C',
click: () => {
mainWindow?.webContents.send('menu:copyTranscription');
safeSend(mainWindow, 'menu:copyTranscription');
},
},
],
Expand All @@ -81,7 +98,7 @@ function createMenu() {
label: 'Toggle History',
accelerator: 'CmdOrCtrl+H',
click: () => {
mainWindow?.webContents.send('menu:toggleHistory');
safeSend(mainWindow, 'menu:toggleHistory');
},
},
],
Expand All @@ -93,14 +110,14 @@ function createMenu() {
label: 'Start Transcription',
accelerator: 'CmdOrCtrl+Enter',
click: () => {
mainWindow?.webContents.send('menu:startTranscription');
safeSend(mainWindow, 'menu:startTranscription');
},
},
{
label: 'Cancel Transcription',
accelerator: 'Escape',
click: () => {
mainWindow?.webContents.send('menu:cancelTranscription');
safeSend(mainWindow, 'menu:cancelTranscription');
},
},
],
Expand Down Expand Up @@ -201,6 +218,7 @@ const createWindow = () => {
sandbox: true,
},
titleBarStyle: 'hiddenInset',
title: APP_DISPLAY_NAME,
trafficLightPosition: { x: 20, y: 20 },
});

Expand Down
48 changes: 43 additions & 5 deletions src/main/ipc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@ import {
generatePdfDocument,
generateMarkdownDocument,
} from '../utils/export-helper';
import { generateFileFingerprint } from '../utils/media-info';
import { safeSend } from '../utils/safe-send';
import { trackEvent, AnalyticsEvents } from '../services/analytics';
import { SUPPORTED_EXTENSIONS } from '../../shared/types';
import type { TranscriptionOptions, SaveFileOptions } from '../../shared/types';

const OPEN_DIALOG_MEDIA_EXTENSIONS = [...SUPPORTED_EXTENSIONS];

export function registerIpcHandlers(getMainWindow: () => BrowserWindow | null) {
ipcMain.handle('dialog:openFile', async () => {
const mainWindow = getMainWindow();
Expand All @@ -29,7 +34,7 @@ export function registerIpcHandlers(getMainWindow: () => BrowserWindow | null) {
filters: [
{
name: 'Audio/Video',
extensions: ['mp3', 'wav', 'm4a', 'mp4', 'mov', 'mkv', 'flac', 'ogg', 'webm'],
extensions: OPEN_DIALOG_MEDIA_EXTENSIONS,
},
],
});
Expand All @@ -48,7 +53,7 @@ export function registerIpcHandlers(getMainWindow: () => BrowserWindow | null) {
filters: [
{
name: 'Audio/Video',
extensions: ['mp3', 'wav', 'm4a', 'mp4', 'mov', 'mkv', 'flac', 'ogg', 'webm'],
extensions: OPEN_DIALOG_MEDIA_EXTENSIONS,
},
],
});
Expand Down Expand Up @@ -91,11 +96,19 @@ export function registerIpcHandlers(getMainWindow: () => BrowserWindow | null) {

ipcMain.handle('file:getInfo', async (_event, filePath: string) => {
try {
const stats = fs.statSync(filePath);
const stats = await fs.promises.stat(filePath);
let fingerprint: string | undefined;
try {
fingerprint = await generateFileFingerprint(filePath, stats.size);
} catch {
fingerprint = undefined;
}
Comment thread
pedrovsiqueira marked this conversation as resolved.

return {
name: path.basename(filePath),
path: filePath,
size: stats.size,
fingerprint,
};
} catch {
return null;
Expand All @@ -114,7 +127,7 @@ export function registerIpcHandlers(getMainWindow: () => BrowserWindow | null) {
ipcMain.handle('models:download', async (_event, modelName: string) => {
try {
const result = await downloadModel(modelName, (progress) => {
getMainWindow()?.webContents.send('models:downloadProgress', progress);
safeSend(getMainWindow(), 'models:downloadProgress', progress);
});
if (result.success) {
trackEvent(AnalyticsEvents.MODEL_DOWNLOADED, { model: modelName });
Expand All @@ -138,7 +151,7 @@ export function registerIpcHandlers(getMainWindow: () => BrowserWindow | null) {
ipcMain.handle('transcribe:start', async (_event, options: TranscriptionOptions) => {
try {
const promise = transcribe(options, (progress) => {
getMainWindow()?.webContents.send('transcribe:progress', progress);
safeSend(getMainWindow(), 'transcribe:progress', progress);
});

currentTranscription = promise;
Expand Down Expand Up @@ -227,4 +240,29 @@ export function registerIpcHandlers(getMainWindow: () => BrowserWindow | null) {
}
await shell.openExternal(url);
});

ipcMain.handle('shell:showItemInFolder', async (_event, filePath: string) => {
try {
if (typeof filePath !== 'string' || filePath.trim().length === 0) {
return { success: false, error: 'Invalid file path' };
}

if (!path.isAbsolute(filePath)) {
return { success: false, error: 'Invalid file path' };
}

const resolvedPath = path.resolve(filePath);

try {
await fs.promises.access(resolvedPath, fs.constants.F_OK);
} catch {
return { success: false, error: 'File not found' };
}

shell.showItemInFolder(resolvedPath);
return { success: true };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
});
}
13 changes: 7 additions & 6 deletions src/main/services/auto-updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { autoUpdater } from 'electron-updater';
import type { BrowserWindow } from 'electron';
import type { UpdateInfo, UpdateProgress } from '../../shared/types';
import { trackEvent, AnalyticsEvents } from './analytics';
import { safeSend } from '../utils/safe-send';
import log from 'electron-log';

autoUpdater.logger = log;
Expand All @@ -24,7 +25,7 @@ export function initAutoUpdater(getMainWindow: () => BrowserWindow | null) {
updateCheckInProgress = true;
log.info('Checking for updates...');
const mainWindow = getMainWindow();
mainWindow?.webContents.send('update:status', {
safeSend(mainWindow, 'update:status', {
status: 'checking',
});
trackEvent(AnalyticsEvents.UPDATE_CHECKING);
Expand All @@ -39,7 +40,7 @@ export function initAutoUpdater(getMainWindow: () => BrowserWindow | null) {
releaseDate: info.releaseDate,
releaseNotes: info.releaseNotes as string | undefined,
};
mainWindow?.webContents.send('update:status', {
safeSend(mainWindow, 'update:status', {
status: 'available',
info: updateInfo,
});
Expand All @@ -50,7 +51,7 @@ export function initAutoUpdater(getMainWindow: () => BrowserWindow | null) {
updateCheckInProgress = false;
log.info('No updates available');
const mainWindow = getMainWindow();
mainWindow?.webContents.send('update:status', {
safeSend(mainWindow, 'update:status', {
status: 'not-available',
});
trackEvent(AnalyticsEvents.UPDATE_NOT_AVAILABLE);
Expand All @@ -60,7 +61,7 @@ export function initAutoUpdater(getMainWindow: () => BrowserWindow | null) {
updateCheckInProgress = false;
log.error('Update error:', error);
const mainWindow = getMainWindow();
mainWindow?.webContents.send('update:status', {
safeSend(mainWindow, 'update:status', {
status: 'error',
error: error.message,
});
Expand All @@ -77,7 +78,7 @@ export function initAutoUpdater(getMainWindow: () => BrowserWindow | null) {
transferred: progressObj.transferred,
total: progressObj.total,
};
mainWindow?.webContents.send('update:status', {
safeSend(mainWindow, 'update:status', {
status: 'downloading',
progress,
});
Expand All @@ -90,7 +91,7 @@ export function initAutoUpdater(getMainWindow: () => BrowserWindow | null) {
releaseDate: info.releaseDate,
releaseNotes: info.releaseNotes as string | undefined,
};
mainWindow?.webContents.send('update:status', {
safeSend(mainWindow, 'update:status', {
status: 'downloaded',
info: updateInfo,
});
Expand Down
6 changes: 4 additions & 2 deletions src/main/services/whisper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ function convertToWav(inputPath: string, outputPath: string): Promise<string> {
const args = [
'-i',
inputPath,
'-vn', // Drop video streams if present
'-ar',
'16000', // 16kHz sample rate (required by Whisper)
'-ac',
Expand Down Expand Up @@ -512,8 +513,9 @@ export function transcribe(
outputBase,
];

// Add language if specified
if (language && language !== 'auto') {
// whisper.cpp defaults to English when -l is omitted.
// Pass 'auto' explicitly to enable language auto-detection.
if (language) {
Comment thread
pedrovsiqueira marked this conversation as resolved.
args.push('-l', language);
}

Expand Down
Loading