fix(dictation): self-host Silero VAD / onnxruntime-web assets
Streaming dictation failed at runtime with "no available backend found /
'text/html' is not a valid JavaScript MIME type": @ricky0123/vad-web 0.0.30
defaults baseAssetPath/onnxWASMBasePath to "./" (relative to the page URL),
so the worklet, Silero model and ORT wasm/mjs were requested against the SPA
catch-all and came back as index.html.
Serve them from a fixed /vad/ instead:
- scripts/copy-vad-assets.mjs copies the 4 runtime assets (vad worklet,
silero_vad_v5.onnx, ort-wasm-simd-threaded.jsep.{mjs,wasm}) from node_modules
into apps/client/public/vad/ (gitignored — the ORT wasm is ~26 MB)
- client dev/build scripts run the copy first so the assets are always present
- useStreamingDictation points both path constants at "/vad/"
Verified: dev server serves all four under /vad/ with HTTP 200 and correct
Content-Type (js/wasm, never text/html); tsc clean. Prod (Docker) build runs
the copy step, so dist/vad/* ships in the image.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -45,3 +45,6 @@ lerna-debug.log*
|
|||||||
|
|
||||||
# TypeScript incremental build artifacts
|
# TypeScript incremental build artifacts
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Self-hosted VAD / onnxruntime-web assets (copied from node_modules at dev/build time)
|
||||||
|
apps/client/public/vad/
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.93.0",
|
"version": "0.93.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "node scripts/copy-vad-assets.mjs && vite",
|
||||||
"build": "tsc && vite build",
|
"build": "node scripts/copy-vad-assets.mjs && tsc && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"format": "prettier --write \"src/**/*.tsx\" \"src/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.tsx\" \"src/**/*.ts\"",
|
||||||
|
|||||||
59
apps/client/scripts/copy-vad-assets.mjs
Normal file
59
apps/client/scripts/copy-vad-assets.mjs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Self-host the @ricky0123/vad-web + onnxruntime-web runtime assets under
|
||||||
|
// apps/client/public/vad/.
|
||||||
|
//
|
||||||
|
// WHY THIS EXISTS:
|
||||||
|
// Both vad-web and onnxruntime-web resolve their assets by URL *at runtime* (the
|
||||||
|
// VAD audio worklet + Silero model, and ORT's wasm/mjs backend). In vad-web
|
||||||
|
// 0.0.30 the default baseAssetPath / onnxWASMBasePath is "./" — i.e. relative to
|
||||||
|
// the current page URL — NOT a CDN. In this SPA that "./" request hits the
|
||||||
|
// client-side catch-all route and gets served index.html (text/html), so the
|
||||||
|
// onnxruntime ESM/wasm backend fails to initialize ("'text/html' is not a valid
|
||||||
|
// JavaScript MIME type"). We fix that by copying the four needed files into
|
||||||
|
// public/vad/ and pointing both path constants at the fixed absolute "/vad/".
|
||||||
|
//
|
||||||
|
// These copies are NOT committed (the ORT wasm is ~26 MB); this script runs
|
||||||
|
// before `dev` and `build` (see package.json) to repopulate them from
|
||||||
|
// node_modules. It is idempotent: it (re)creates the dir and overwrites.
|
||||||
|
|
||||||
|
import { createRequire } from "node:module";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import path from "node:path";
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const outDir = path.join(here, "..", "public", "vad");
|
||||||
|
|
||||||
|
// vad-web exposes ./package.json, so derive its dist dir from there.
|
||||||
|
const vadDist = path.join(
|
||||||
|
path.dirname(require.resolve("@ricky0123/vad-web/package.json")),
|
||||||
|
"dist",
|
||||||
|
);
|
||||||
|
|
||||||
|
// onnxruntime-web's "exports" map does NOT expose ./package.json, so resolving
|
||||||
|
// it would throw ERR_PACKAGE_PATH_NOT_EXPORTED. It DOES export the exact asset
|
||||||
|
// subpaths we need, so resolve those files directly.
|
||||||
|
const ortMjs = require.resolve(
|
||||||
|
"onnxruntime-web/ort-wasm-simd-threaded.jsep.mjs",
|
||||||
|
);
|
||||||
|
const ortWasm = require.resolve(
|
||||||
|
"onnxruntime-web/ort-wasm-simd-threaded.jsep.wasm",
|
||||||
|
);
|
||||||
|
|
||||||
|
// [absolute source path, output filename]
|
||||||
|
const files = [
|
||||||
|
[path.join(vadDist, "vad.worklet.bundle.min.js"), "vad.worklet.bundle.min.js"],
|
||||||
|
[path.join(vadDist, "silero_vad_v5.onnx"), "silero_vad_v5.onnx"],
|
||||||
|
[ortMjs, "ort-wasm-simd-threaded.jsep.mjs"],
|
||||||
|
[ortWasm, "ort-wasm-simd-threaded.jsep.wasm"],
|
||||||
|
];
|
||||||
|
|
||||||
|
fs.mkdirSync(outDir, { recursive: true });
|
||||||
|
for (const [src, name] of files) {
|
||||||
|
if (!fs.existsSync(src)) {
|
||||||
|
console.error(`[copy-vad-assets] missing source: ${src}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
fs.copyFileSync(src, path.join(outDir, name));
|
||||||
|
console.log(`[copy-vad-assets] ${name}`);
|
||||||
|
}
|
||||||
@@ -32,14 +32,17 @@ interface UseStreamingDictationResult {
|
|||||||
// Sample rate of the audio MicVAD hands to onSpeechEnd (Silero VAD runs at 16k).
|
// Sample rate of the audio MicVAD hands to onSpeechEnd (Silero VAD runs at 16k).
|
||||||
const VAD_SAMPLE_RATE = 16000;
|
const VAD_SAMPLE_RATE = 16000;
|
||||||
|
|
||||||
// Asset paths for the VAD worklet and the onnxruntime WASM binaries. For this
|
// Asset paths for the VAD worklet/Silero model and the onnxruntime-web WASM
|
||||||
// prototype they are left undefined so the library loads its bundled assets from
|
// binaries. vad-web 0.0.30's default asset path is "./" (relative to the current
|
||||||
// its default CDN — this avoids fragile rolldown asset-copy config. For a
|
// page URL), NOT a CDN — in this SPA that request hits the client-side catch-all
|
||||||
// self-hosted / offline / privacy build, copy the vad-web `dist` worklet + the
|
// route and returns index.html (text/html), so the onnxruntime ESM/wasm backend
|
||||||
// `*.onnx` model and the onnxruntime-web `*.wasm` files into
|
// fails to initialize. We instead self-host the four needed files (the vad-web
|
||||||
// `apps/client/public/vad/` and set these to that local path (e.g. "/vad/").
|
// worklet + `silero_vad_v5.onnx` model and the onnxruntime-web `*.jsep.mjs`/
|
||||||
const VAD_BASE_ASSET_PATH: string | undefined = undefined;
|
// `*.jsep.wasm`) under `apps/client/public/vad/` — populated by
|
||||||
const VAD_ONNX_WASM_BASE_PATH: string | undefined = undefined;
|
// `scripts/copy-vad-assets.mjs`, which runs before `dev`/`build` — and point both
|
||||||
|
// paths at the fixed absolute "/vad/".
|
||||||
|
const VAD_BASE_ASSET_PATH: string | undefined = "/vad/";
|
||||||
|
const VAD_ONNX_WASM_BASE_PATH: string | undefined = "/vad/";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Streaming variant of useDictation. Detects speech with a real (Silero) VAD and,
|
* Streaming variant of useDictation. Detects speech with a real (Silero) VAD and,
|
||||||
|
|||||||
Reference in New Issue
Block a user