Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,15 @@ Marked issues are numbered `MX-001…`; items that come from narration alone are
</tr>
</table>

## Local Transcription

On desktop startup, MarkuprPlus downloads the multilingual Whisper **tiny**
model (~75 MB) in the background if no local model is installed. Existing
models are reused. Download progress and retryable errors appear under
**Settings > Advanced > Local Transcription**. Interrupted downloads resume on
the next startup; offline or failed downloads do not block opening the app.
Once downloaded, local transcription works without an internet connection.

## Interface Language

The desktop interface defaults to **English**, including when upgrading from an
Expand Down
10 changes: 10 additions & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2043,6 +2043,13 @@ app.whenReady().then(async () => {
setupIPC();
setupElectronTestHarnessIPC();

if ((!electronTestHarnessAllowed() || process.env.MARKUPRX_E2E_DOWNLOAD_MODELS === '1')
&& process.env.MARKUPRX_PACKAGE_SMOKE !== '1') {
void modelDownloadManager.ensureDefaultModel().catch((error: unknown) => {
console.warn('[Main] Automatic Whisper download failed. Retry in Settings > Advanced > Local Transcription:', error);
});
}

// 11. Configure session controller event callbacks
sessionController.setEventCallbacks({
onStateChange: handleSessionStateChange,
Expand Down Expand Up @@ -2090,6 +2097,9 @@ app.on('window-all-closed', () => {
// Handle before quit
app.on('before-quit', () => {
isQuitting = true;
for (const model of modelDownloadManager.getAvailableModels()) {
modelDownloadManager.cancelDownload(model.name);
}
});

// Handle app quit
Expand Down
61 changes: 21 additions & 40 deletions src/main/ipc/windowHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { ipcMain, app } from 'electron';
import { tierManager } from '../transcription/TierManager';
import { modelDownloadManager } from '../transcription/ModelDownloadManager';
import { DEFAULT_DOWNLOAD_MODEL, modelDownloadManager } from '../transcription/ModelDownloadManager';
import { whisperService } from '../transcription/WhisperService';
import type { WhisperModel } from '../transcription/types';
import { POPOVER_SIZES } from '../windows';
Expand All @@ -18,9 +18,25 @@ import {
} from '../../shared/types';
import type { IpcContext } from './types';

export function registerWindowHandlers(ctx: IpcContext): void {
export function registerWindowHandlers(ctx: Pick<IpcContext, 'getMainWindow' | 'getPopover' | 'getWindowsTaskbar'>): void {
const { getMainWindow, getPopover, getWindowsTaskbar } = ctx;

const sendDownloadEvent = (channel: string, payload: unknown): void => {
const window = getMainWindow();
if (window && !window.isDestroyed()) window.webContents.send(channel, payload);
};
// Startup downloads have no invoking renderer, so observe the manager globally.
modelDownloadManager.onProgress((progress) => {
sendDownloadEvent(IPC_CHANNELS.WHISPER_DOWNLOAD_PROGRESS, progress);
});
modelDownloadManager.onComplete((result) => {
if (result.success) whisperService.setModelPath(result.path);
sendDownloadEvent(IPC_CHANNELS.WHISPER_DOWNLOAD_COMPLETE, { model: result.model, path: result.path });
});
modelDownloadManager.onError((error, model) => {
sendDownloadEvent(IPC_CHANNELS.WHISPER_DOWNLOAD_ERROR, { model, error: error.message });
});

// -------------------------------------------------------------------------
// App Version
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -216,15 +232,16 @@ export function registerWindowHandlers(ctx: IpcContext): void {
}

const defaultModel = hasAnyModel ? modelDownloadManager.getDefaultModel() : null;
const recommendedModel = 'tiny';
const recommendedInfo = modelDownloadManager.getModelInfo('tiny');
const recommendedModel = DEFAULT_DOWNLOAD_MODEL;
const recommendedInfo = modelDownloadManager.getModelInfo(recommendedModel);

return {
hasAnyModel,
defaultModel,
downloadedModels,
recommendedModel,
recommendedModelSizeMB: recommendedInfo.sizeMB,
downloadStatus: modelDownloadManager.getDownloadStatus(recommendedModel),
};
});

Expand All @@ -251,43 +268,7 @@ export function registerWindowHandlers(ctx: IpcContext): void {
return { success: false, error: 'Invalid model name' };
}
try {
const unsubProgress = modelDownloadManager.onProgress((progress) => {
getMainWindow()?.webContents.send(IPC_CHANNELS.WHISPER_DOWNLOAD_PROGRESS, {
model: progress.model,
downloadedBytes: progress.downloadedBytes,
totalBytes: progress.totalBytes,
percent: progress.percent,
speedBps: progress.speedBps,
estimatedSecondsRemaining: progress.estimatedSecondsRemaining,
});
});

const unsubComplete = modelDownloadManager.onComplete((result) => {
getMainWindow()?.webContents.send(IPC_CHANNELS.WHISPER_DOWNLOAD_COMPLETE, {
model: result.model,
path: result.path,
});
unsubProgress();
unsubComplete();
unsubError();
});

const unsubError = modelDownloadManager.onError((error, errorModel) => {
getMainWindow()?.webContents.send(IPC_CHANNELS.WHISPER_DOWNLOAD_ERROR, {
model: errorModel,
error: error.message,
});
unsubProgress();
unsubComplete();
unsubError();
});

const result = await modelDownloadManager.downloadModel(model as WhisperModel);

if (result.success) {
whisperService.setModelPath(result.path);
}

return { success: result.success };
} catch (error) {
console.error('[Main] Failed to download Whisper model:', error);
Expand Down
Loading
Loading