From 4e32f9b7bd04ad505a2d6c2186254d8e87209698 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 4 Apr 2026 22:11:54 +0700 Subject: [PATCH] fix: copy Silero VAD model out of asar so native code can read it The bundled silero_vad.onnx lives inside Electron's app.asar which native C++ (sherpa-onnx) cannot open via OS file I/O. On first voice activation, copy the model to ~/.paseo/models/local-speech/silero-vad/ using Node.js fs (asar-aware) and point the native code there. --- .../server/speech/providers/local/runtime.ts | 12 +++++-- .../local/sherpa/silero-vad-provider.ts | 35 ++++++++++++++++++- .../local/sherpa/silero-vad-session.ts | 2 +- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/server/src/server/speech/providers/local/runtime.ts b/packages/server/src/server/speech/providers/local/runtime.ts index 4021aeb99..0fa7d2c3b 100644 --- a/packages/server/src/server/speech/providers/local/runtime.ts +++ b/packages/server/src/server/speech/providers/local/runtime.ts @@ -22,7 +22,7 @@ import { SherpaParakeetRealtimeTranscriptionSession } from "./sherpa/sherpa-para import { SherpaRealtimeTranscriptionSession } from "./sherpa/sherpa-realtime-session.js"; import { SherpaOnnxSTT } from "./sherpa/sherpa-stt.js"; import { SherpaOnnxTTS } from "./sherpa/sherpa-tts.js"; -import { SherpaSileroTurnDetectionProvider } from "./sherpa/silero-vad-provider.js"; +import { ensureSileroVadModel, SherpaSileroTurnDetectionProvider } from "./sherpa/silero-vad-provider.js"; type LocalSttEngine = | { kind: "offline"; engine: SherpaOfflineRecognizerEngine } @@ -236,7 +236,15 @@ export async function initializeLocalSpeechServices(params: { providers.voiceTurnDetection.enabled !== false && providers.voiceTurnDetection.provider === "local" ) { - turnDetectionService = new SherpaSileroTurnDetectionProvider({}, logger); + let vadModelPath: string | undefined; + if (localConfig) { + try { + vadModelPath = await ensureSileroVadModel(localConfig.modelsDir, logger); + } catch (err) { + logger.warn({ err }, "Failed to provision Silero VAD model, falling back to bundled"); + } + } + turnDetectionService = new SherpaSileroTurnDetectionProvider({ modelPath: vadModelPath }, logger); } if (providers.voiceStt.enabled !== false && providers.voiceStt.provider === "local") { diff --git a/packages/server/src/server/speech/providers/local/sherpa/silero-vad-provider.ts b/packages/server/src/server/speech/providers/local/sherpa/silero-vad-provider.ts index 886fb66e2..5c93323f1 100644 --- a/packages/server/src/server/speech/providers/local/sherpa/silero-vad-provider.ts +++ b/packages/server/src/server/speech/providers/local/sherpa/silero-vad-provider.ts @@ -1,10 +1,43 @@ +import { copyFile, mkdir, stat } from "node:fs/promises"; +import path from "node:path"; + import type { Logger } from "pino"; import type { TurnDetectionProvider, TurnDetectionSession, } from "../../../turn-detection-provider.js"; -import { SherpaSileroVadSession, type SherpaSileroVadSessionConfig } from "./silero-vad-session.js"; +import { + resolveBundledSileroVadModelPath, + SherpaSileroVadSession, + type SherpaSileroVadSessionConfig, +} from "./silero-vad-session.js"; + +const SILERO_VAD_DIR = "silero-vad"; +const SILERO_VAD_FILE = "silero_vad.onnx"; + +/** + * Ensure the Silero VAD ONNX model exists in modelsDir where native code can + * read it. The bundled asset lives inside Electron's app.asar which native C++ + * cannot open, so we copy it out on first run using Node.js fs (asar-aware). + */ +export async function ensureSileroVadModel(modelsDir: string, logger: Logger): Promise { + const destDir = path.join(modelsDir, SILERO_VAD_DIR); + const destPath = path.join(destDir, SILERO_VAD_FILE); + + try { + const s = await stat(destPath); + if (s.isFile() && s.size > 0) return destPath; + } catch { + // not present yet + } + + const bundledPath = resolveBundledSileroVadModelPath(); + await mkdir(destDir, { recursive: true }); + await copyFile(bundledPath, destPath); + logger.info({ destPath }, "Copied Silero VAD model to models directory"); + return destPath; +} export class SherpaSileroTurnDetectionProvider implements TurnDetectionProvider { public readonly id = "local" as const; diff --git a/packages/server/src/server/speech/providers/local/sherpa/silero-vad-session.ts b/packages/server/src/server/speech/providers/local/sherpa/silero-vad-session.ts index e2fd52df8..c212d7204 100644 --- a/packages/server/src/server/speech/providers/local/sherpa/silero-vad-session.ts +++ b/packages/server/src/server/speech/providers/local/sherpa/silero-vad-session.ts @@ -34,7 +34,7 @@ type SherpaVadModule = { CircularBuffer: new (capacity: number) => SherpaCircularBufferHandle; }; -function resolveBundledSileroVadModelPath(): string { +export function resolveBundledSileroVadModelPath(): string { return fileURLToPath(new URL("./assets/silero_vad.onnx", import.meta.url)); }