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.
This commit is contained in:
Mohamed Boudra
2026-04-04 22:11:54 +07:00
parent 744ca7a2bc
commit 4e32f9b7bd
3 changed files with 45 additions and 4 deletions
@@ -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") {
@@ -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<string> {
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;
@@ -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));
}