Skip to content
Open
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: 5 additions & 4 deletions LOCAL_WHISPER_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ The first transcription will download the model automatically.
Local Whisper can run on your GPU for much faster transcription:

- **macOS**: Metal acceleration is built in — no setup needed on Apple Silicon
- **NVIDIA (Windows/Linux)**: one-click CUDA runtime download from the GPU card in the transcription model picker
- **AMD / Intel (Windows/Linux)**: one-click Vulkan runtime download from the same GPU card — covers Radeon and Arc/integrated GPUs
- **NVIDIA (Windows/Linux)**: CUDA acceleration; Linux requires a current compatible NVIDIA driver, but not the CUDA Toolkit
- **AMD / Intel (Windows/Linux)**: Vulkan acceleration for Radeon, Arc, and integrated GPUs
- **NVIDIA alternative**: Vulkan is also supported and is used automatically when its runtime is installed and CUDA fails

The GPU runtime is downloaded on demand with SHA-256-verified checksums. If the GPU server crashes or fails to start (unsupported GPU, out of VRAM), OpenWhispr automatically falls back to CPU transcription and shows a notice — dictation keeps working.
GPU runtimes are downloaded on demand with SHA-256-verified checksums. If one fails, OpenWhispr uses the next available backend and finally CPU, so dictation keeps working.

## How It Works

Expand All @@ -45,7 +46,7 @@ OpenWhispr uses whisper.cpp, a high-performance C++ implementation of OpenAI's W

- **Disk Space**: 75MB–3GB depending on model
- **RAM**: 1GB–10GB depending on model
- **No additional dependencies required** - whisper.cpp is bundled in packaged builds
- **GPU driver**: required for GPU acceleration; the CUDA Toolkit is not required

## Running From Source

Expand Down
34 changes: 32 additions & 2 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -573,10 +573,40 @@ function initializeCoreManagers() {
// with no pack on disk only happens via such data loss. recordOnce gates
// each pack to one notice so a dismissed toast doesn't return every launch.
const gpuPacks = [
{ manager: whisperCudaManager, enabledEnvVar: "WHISPER_CUDA_ENABLED", group: "whisper" },
{ manager: whisperVulkanManager, enabledEnvVar: "WHISPER_VULKAN_ENABLED", group: "whisper" },
{
manager: whisperCudaManager,
enabledEnvVar: "WHISPER_CUDA_ENABLED",
group: "whisper",
backend: "cuda",
},
{
manager: whisperVulkanManager,
enabledEnvVar: "WHISPER_VULKAN_ENABLED",
group: "whisper",
backend: "vulkan",
},
{ manager: llamaVulkanManager, enabledEnvVar: "LLAMA_VULKAN_ENABLED" },
];
// A pack whose backend failed here and then vanished (a distro launcher
// deletes it, #2168) was no loss: drop its flag as a delete would, so no
// notice names it, even after an upgrade clears the remembered failure.
const failedBackends = WhisperManager.resolveFailedGpuBackends(process.env.WHISPER_GPU_FAILED);
for (const { manager, enabledEnvVar, backend } of gpuPacks) {
if (
failedBackends.includes(backend) &&
process.env[enabledEnvVar] === "true" &&
!manager.isDownloaded() &&
!manager.needsUpdate()
) {
delete process.env[enabledEnvVar];
environmentManager.removeKeyFromEnvFile(enabledEnvVar).catch((err) => {
debugLogger.error("Failed to clear a vanished GPU pack's flag from .env", {
key: enabledEnvVar,
error: err.message,
});
});
}
}
const orphanedPacks = detectOrphanedGpuPacks(gpuPacks);
if (orphanedPacks.length > 0) {
require("./src/helpers/gpuPackMigrationNotice").recordOnce(orphanedPacks);
Expand Down
12 changes: 8 additions & 4 deletions src/components/TranscriptionModelPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ export default function TranscriptionModelPicker({
const [gpuActivating, setGpuActivating] = useState(false);
// Live truth from the running server; "active" is never inferred from a download
const [gpuActive, setGpuActive] = useState(false);
const gpuBackendName = gpuBackend === "cuda" ? "CUDA" : "Vulkan";

useEffect(() => {
const organization = getSelectedASROrganization(selectedLocalProvider, selectedLocalModel);
Expand Down Expand Up @@ -1477,21 +1478,21 @@ export default function TranscriptionModelPicker({
<>
<span className="inline-block w-1.5 h-1.5 rounded-full shrink-0 bg-primary animate-pulse" />
<span className="text-xs font-medium text-foreground">
{t("gpu.activating")}
{t("gpu.activatingBackend", { backend: gpuBackendName })}
</span>
</>
) : gpuActive ? (
<>
<Check size={13} className="text-success" />
<span className="text-xs font-medium text-foreground">
{t("gpu.active")}
{t("gpu.activeBackend", { backend: gpuBackendName })}
</span>
</>
) : (
<>
<span className="inline-block w-1.5 h-1.5 rounded-full shrink-0 bg-primary" />
<span className="text-xs font-medium text-foreground">
{t("gpu.ready")}
{t("gpu.readyBackend", { backend: gpuBackendName })}
</span>
</>
)}
Expand Down Expand Up @@ -1521,14 +1522,17 @@ export default function TranscriptionModelPicker({
<p className="text-xs font-medium text-foreground">
{t("gpu.transcriptionBanner")}
</p>
<p className="mt-0.5 text-xs leading-snug text-muted-foreground">
{t(gpuBackend === "cuda" ? "gpu.cudaDescription" : "gpu.vulkanDescription")}
</p>
<div className="flex items-center gap-2 mt-1.5">
<Button
onClick={handleGpuDownload}
size="sm"
variant="default"
className="h-6 px-2.5 text-xs"
>
{t("gpu.enableButton")}
{t("gpu.enableBackend", { backend: gpuBackendName })}
</Button>
<button
onClick={() => setGpuDismissed(true)}
Expand Down
33 changes: 28 additions & 5 deletions src/helpers/ipcHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -1383,13 +1383,23 @@ class IPCHandlers {
}

_attachWhisperServerListeners(serverManager) {
serverManager.on("cuda-fallback", ({ reason }) => {
// Landing on the Vulkan pack or a bundled GPU build (#2168) keeps the GPU:
// no "switched to CPU" pop-up, but every GPU card re-reads to follow the server
serverManager.on("cuda-fallback", ({ reason, fallbackBackend }) => {
this._recordWhisperGpuFailure("cuda", reason);
broadcastToWindows("cuda-fallback-notification", {});
if (fallbackBackend !== "cpu") {
broadcastToWindows("whisper-gpu-status-changed");
} else {
broadcastToWindows("cuda-fallback-notification", {});
}
});
serverManager.on("gpu-fallback", ({ reason }) => {
serverManager.on("gpu-fallback", ({ reason, fallbackBackend }) => {
this._recordWhisperGpuFailure("vulkan", reason);
broadcastToWindows("gpu-fallback-notification", {});
if (fallbackBackend !== "cpu") {
broadcastToWindows("whisper-gpu-status-changed");
} else {
broadcastToWindows("gpu-fallback-notification", {});
}
});
// Persist the discrete-GPU pin so later launches spawn pinned directly
// instead of paying a second Vulkan cold start. See #1606.
Expand All @@ -1399,6 +1409,17 @@ class IPCHandlers {
serverManager.on("vulkan-device-pin-cleared", () => {
this._syncStartupEnv({}, ["WHISPER_VULKAN_DEVICE"]);
});
// The GPU card and banner stop offering a pack (see _whisperBundledGpu)
serverManager.on("bundled-gpu", () => broadcastToWindows("whisper-gpu-status-changed"));
}

// A bundled whisper-server built for the GPU runs on it with no pack (#2168),
// so no pack is offered.
_whisperBundledGpu() {
const { serverManager } = this.whisperManager;
return (
!serverManager.useCuda && !serverManager.useVulkan && serverManager.getStatus().gpuAccelerated
);
}

// Remember the failed backend, and the error line that explains it, so the
Expand Down Expand Up @@ -1452,7 +1473,7 @@ class IPCHandlers {
// Every pack download or delete and every Retry ends here, already saved.
// Tell every window, not just the caller's: Retry on the fallback pop-up
// runs in the dictation window, and Settings keeps up to three GPU cards
// mounted (#1736). A fallback is announced by its own notification.
// mounted (#1736). A fallback is announced where it is recorded.
broadcastToWindows("whisper-gpu-status-changed");
return !!modelName;
}
Expand Down Expand Up @@ -3669,6 +3690,7 @@ class IPCHandlers {
gpuInfo,
...this._whisperGpuFailureStatus("cuda"),
inUse: this.whisperManager.resolveGpuPackInUse() === "cuda",
bundledGpu: this._whisperBundledGpu(),
};
});

Expand Down Expand Up @@ -3733,6 +3755,7 @@ class IPCHandlers {
hasNvidiaGpu: gpuInfo.hasNvidiaGpu,
...this._whisperGpuFailureStatus("vulkan"),
inUse: this.whisperManager.resolveGpuPackInUse() === "vulkan",
bundledGpu: this._whisperBundledGpu(),
};
});

Expand Down
16 changes: 11 additions & 5 deletions src/helpers/whisper.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,17 @@ class WhisperManager {
(process.env.WHISPER_CUDA_ENABLED || "").toLowerCase() !== "false" &&
!failed.includes("cuda") &&
!!this._cudaBinaryManager?.isDownloaded();
const useVulkan =
!useCuda &&
const vulkanAvailable =
(process.env.WHISPER_VULKAN_ENABLED || "").toLowerCase() !== "false" &&
!failed.includes("vulkan") &&
!!this._vulkanBinaryManager?.isDownloaded();
return { useCuda, useVulkan };
// Always set fallbackToVulkan: callers merge this over the last start's
// options, so an omitted false would keep a stale true
return {
useCuda,
useVulkan: !useCuda && vulkanAvailable,
fallbackToVulkan: useCuda && vulkanAvailable,
};
}

// The pack the settings card describes, so the card never disagrees with the
Expand Down Expand Up @@ -180,7 +185,8 @@ class WhisperManager {

// Pre-warm whisper-server if local mode enabled (eliminates 2-5s cold-start delay)
const { localTranscriptionProvider, whisperModel } = settings;
const { useCuda, useVulkan } = this.resolveGpuStartOptions();
const gpuOptions = this.resolveGpuStartOptions();
const { useCuda, useVulkan } = gpuOptions;

if (
localTranscriptionProvider === "whisper" &&
Expand All @@ -199,7 +205,7 @@ class WhisperManager {

try {
const serverStartTime = Date.now();
await this.serverManager.start(modelPath, { useCuda, useVulkan });
await this.serverManager.start(modelPath, gpuOptions);
this.currentServerModel = whisperModel;

debugLogger.info("whisper-server pre-warmed successfully", {
Expand Down
4 changes: 4 additions & 0 deletions src/helpers/whisperCudaManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ class WhisperCudaManager extends GpuBinaryManager {
binaryName: "whisper-server-linux-x64-cuda",
outputName: "whisper-server-linux-x64-cuda",
libPattern: /\.so(\.\d+)*$/,
// The CUDA 12 runtime the binary links, so hosts with another CUDA major
// (or no toolkit) can run it; only the driver's libcuda.so.1 comes from
// the system. Packs from whisper.cpp 0.0.10 and older lack them.
requiredLibraries: ["libcudart.so.12", "libcublas.so.12", "libcublasLt.so.12"],
},
},
});
Expand Down
Loading
Loading