@gjsify/vad 0.0.0-stage → 0.64.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,104 @@
1
- # Temporary Holding Version
1
+ # @gjsify/vad
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Voice activity detection for GJS: [Silero VAD](https://github.com/snakers4/silero-vad) running on [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx) through [`@gjsify/sherpa-native`](../sherpa-native#readme). It answers two questions about an audio stream — **when did speech start**, and **when did a stretch of speech end** — and nothing else. Linux x64 only today. Tier 3 (experimental, [ADR 0003](../../../docs/adr/0003-package-tiering.md)) until promoted. See [ADR 0127](../../../docs/adr/0127-speech-recognition-on-local-engines.md).
4
+
5
+ Part of the [gjsify](https://github.com/gjsify/gjsify) project — Node.js and Web APIs for GJS (GNOME JavaScript).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ gjsify install @gjsify/vad
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ The model is not bundled, so the first run fetches it (2.2 MiB from upstream's release page, verified by size and SHA-256 before it reaches disk):
16
+
17
+ ```typescript
18
+ import { available, install, createVad } from '@gjsify/vad';
19
+ import { openPcmSource } from '@gjsify/media-capture';
20
+
21
+ if (available() === 'downloadable') await install();
22
+
23
+ const vad = createVad(); // 16 kHz mono, 300 ms of silence ends a stretch
24
+ const source = openPcmSource({ sampleRate: 16000, format: 'S16LE' });
25
+
26
+ for await (const event of vad.run(source)) {
27
+ if (event.type === 'speech-start') showListeningIndicator();
28
+ else transcribe(event.samples); // Float32Array, mono, [-1, 1]
29
+ }
30
+ vad.close();
31
+ ```
32
+
33
+ Feeding chunks by hand instead of handing over a source — any chunk length, the engine buffers the remainder:
34
+
35
+ ```typescript
36
+ const vad = createVad({ model: 'silero-v5' });
37
+ vad.open();
38
+ for (const chunk of chunks) {
39
+ for (const event of vad.accept(chunk, 'S16LE')) handle(event);
40
+ }
41
+ for (const event of vad.flush()) handle(event); // not optional, see below
42
+ vad.close();
43
+ ```
44
+
45
+ `flush()` is not optional. The last utterance of a file or a stopped recording is followed by no silence, so nothing closes it and without a flush it is never reported at all.
46
+
47
+ ## The two events
48
+
49
+ | | `speech-start` | `speech-end` |
50
+ |---|---|---|
51
+ | when | the detector *confirms* speech | a stretch has ended |
52
+ | carries | `at` — stream position in seconds | `start`, `end`, `duration`, `startSample`, `samples` |
53
+
54
+ `speech-start.at` is the **live edge** and it lags the acoustic onset: the detector has to hear `minSpeechMs` of speech before it commits. Measured on two 16 kHz recordings with the defaults, it confirmed at 0.67 s and 0.38 s for onsets that the matching `speech-end` later reported at 0.32 s and 0.03 s — about 0.35 s behind in both. That is the right behaviour for a "listening" indicator, which must not flicker on a cough; for *trimming* audio use `speech-end.start`, which is the onset itself.
55
+
56
+ A burst too short to keep (< `minSpeechMs`) produces a `speech-start` with no `speech-end`. It stays that way: inventing a zero-length end would make a cough indistinguishable from a word.
57
+
58
+ ## What this package refuses to say
59
+
60
+ Whether a pause means the speaker is **finished**. That is a question about a conversation — how long this speaker usually hesitates, whether the sentence sounds complete, whether the app may interrupt — and it belongs to a turn detector above this layer, with its own policy. A VAD measures acoustics, so there is deliberately no `turn-end` to reach for.
61
+
62
+ `createFakeVadEngine()` is exported for exactly that consumer: a scripted engine that drives the event machine without a model or an inference run.
63
+
64
+ ## Defaults
65
+
66
+ `createVad()` takes `minSilenceMs` **300** (ADR 0127 decision 10, not Silero's 550 — the whole wait is added to every reply), `minSpeechMs` 250, `maxSpeechMs` 20 000, `threshold` 0.5, `sampleRate` 16 000, `windowSize` 512, `numThreads` 1, `bufferSeconds` 30. A `maxSpeechMs` cut appears as a `speech-end` immediately followed by a fresh `speech-start`.
67
+
68
+ ## Models
69
+
70
+ `available()` answers in the names the Web Speech on-device additions use: `available`, `downloading`, `downloadable`, `unavailable`. `unavailable` means there is **no native engine** on this host, not a missing file — fetching a model for an engine that is not installed is the one outcome a download prompt must never produce.
71
+
72
+ ```typescript
73
+ import { listModels, install, uninstall, modelCacheDir } from '@gjsify/vad';
74
+
75
+ listModels(); // every entry with its availability, for a settings panel
76
+ await install('silero-v4', { onProgress: (p) => console.log(p.received, p.total) });
77
+ uninstall('silero-v4');
78
+ modelCacheDir(); // ~/.cache/gjsify/speech-models, shared with the other speech packages
79
+ ```
80
+
81
+ `VadOptions.model` takes a manifest id (`silero-v5`, `silero-v4`) or a path — anything containing a path separator is used as given, which is how you point at a model this manifest does not know. An id must already be cached: resolving must not start a download behind a synchronous constructor.
82
+
83
+ The models are MIT-licensed by their authors and are **not** part of this package's licence.
84
+
85
+ ## Without a native engine
86
+
87
+ ```typescript
88
+ import { hasVadBackend, describeVadBackend } from '@gjsify/vad';
89
+
90
+ if (!hasVadBackend()) console.log(describeVadBackend());
91
+ ```
92
+
93
+ `hasVadBackend()` is the quiet check. `describeVadBackend()` names the engine and onnxruntime versions, or says why there is none and which `@gjsify/sherpa-native-<os>-<arch>` to install.
94
+
95
+ ## Audio input
96
+
97
+ `run()` accepts `@gjsify/media-capture`'s `openPcmSource()` **structurally** — neither package depends on the other, because a VAD that drags GStreamer in would be unusable on a stream that came from a file or a socket. Any async iterable of chunks works; a source that reports a `sampleRate` different from the detector's is refused, because resampled audio read at the wrong rate does not fail, it just reads as silence.
98
+
99
+ `Float32Array` and `Int16Array` chunks carry their own layout. A raw `Uint8Array` needs an explicit `'S16LE'` or `'F32LE'`: both are plausible for a byte buffer and guessing wrong produces audio that is silently wrong.
100
+
101
+ ## Notes
102
+
103
+ - `gjsify run test` covers the event machine and the model cache with no engine, no model and no network. The real-engine suite skips unless a prebuild and a cached model are present, is file-only and never opens a microphone or plays sound; its speech leg additionally needs `GJSIFY_VAD_TEST_WAV` to name a 16 kHz mono PCM16 file, since no speech fixture is committed.
104
+ - One detector, one stream. `reset()` reuses it across unrelated recordings; `close()` is final.
@@ -0,0 +1 @@
1
+ var e=Object.defineProperty,__name=(t,n)=>e(t,`name`,{value:n,configurable:!0});export{__name};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{getLoadError as e,hasNativeSherpa as t,loadNativeSherpa as n}from"@gjsify/sherpa-native";import{hostOs as r}from"@gjsify/utils/core";const i=`@gjsify/sherpa-native`,a=`[@gjsify/vad] No voice activity detection on this host — the GjsifySherpa-1.0 typelib is absent. Install ${i}-<os>-<arch> for this machine (linux-x64 only today). Gate on hasVadBackend().`,o=`[@gjsify/vad] The ${i} bridge failed to load — a fault on this host, NOT a missing prebuild. Voice activity detection is unavailable until it is fixed.`;let s=null,c=!1;function isNoGiError(e){return e instanceof Error&&e.message.includes(`GObject-Introspection`)}function _diagnoseVadLoadError(e){return isNoGiError(e)?{status:`absent`,diagnostic:null}:e instanceof Error&&e.name===`NativeLibraryLoadError`?{status:`failed`,diagnostic:o}:{status:`absent`,diagnostic:`${a} (${e instanceof Error?e.message:e})`}}function loadVadBackend(t={}){if(s!==null){if(t.load||t.loadError||t.hostOs)throw Error(`loadVadBackend(options) called after the probe already ran — the probe is cached per process, so the override would be ignored. Call _resetVadBackendCache() first (tests only).`);return s}let i=(t.hostOs??r)();if(i===`darwin`||i===`win32`)return s={status:`absent`,module:null,error:null,diagnostic:`[@gjsify/vad] No voice activity detection on this OS — the sherpa-onnx bridge is built for Linux only.`},s;let a=(t.load??n)();if(a===null){let n=(t.loadError??e)(),{status:r,diagnostic:i}=_diagnoseVadLoadError(n);return s={status:r,module:null,error:n,diagnostic:i},s}return s={status:`sherpa`,module:a,error:null,diagnostic:null},s}function hasVadBackend(){return s===null?t()&&loadVadBackend().status===`sherpa`:s.status===`sherpa`}function reportVadBackendOnce(e){c||e.diagnostic===null||(c=!0,e.status===`failed`?console.error(e.diagnostic,e.error):console.warn(e.diagnostic))}function describeVadBackend(){let e=loadVadBackend();if(e.status!==`sherpa`)return`none (${e.status}): ${e.diagnostic??`not available on this target`}`;let t=e.module;return`sherpa-onnx ${t.version()} on onnxruntime ${t.onnxruntime_version()} (${i})`}function _resetVadBackendCache(){s=null,c=!1}function createSherpaVadEngine(e){let t=loadVadBackend();if(t.status!==`sherpa`)throw reportVadBackendOnce(t),Error(t.diagnostic??`No native VAD engine on this host (${t.status})`);let n=new t.module.Vad({model:e.model,threshold:e.threshold,min_silence_duration:e.minSilenceSeconds,min_speech_duration:e.minSpeechSeconds,max_speech_duration:e.maxSpeechSeconds,sample_rate:e.sampleRate,window_size:e.windowSize,num_threads:e.numThreads,buffer_size_in_seconds:e.bufferSeconds,enable_debug:e.debug});return{open:()=>n.open(),accept:e=>n.accept(e),isSpeech:()=>n.is_speech(),hasSegment:()=>n.has_segment(),nextSegment:()=>{let[e,t,r]=n.next_segment();return e?{samples:t instanceof Float32Array?t:Float32Array.from(t),startSample:r}:null},flush:()=>n.flush(),reset:()=>n.reset(),close:()=>n.close()}}export{_diagnoseVadLoadError,_resetVadBackendCache,createSherpaVadEngine,describeVadBackend,hasVadBackend,loadVadBackend,reportVadBackendOnce};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";function createFakeVadEngine(){let e=[],t=!1,n={accepted:0,chunks:0,opened:!1,flushed:0,resets:0,closed:!1};return{get accepted(){return n.accepted},get chunks(){return n.chunks},get opened(){return n.opened},get flushed(){return n.flushed},get resets(){return n.resets},get closed(){return n.closed},setSpeech(e){t=e},pushSegment(t,n){e.push({samples:new Float32Array(n).fill(.5),startSample:t})},open(){n.opened=!0},accept(e){if(!n.opened)throw Error(`fake VAD engine: accept() before open()`);n.accepted+=e.length,n.chunks++},isSpeech(){return t},hasSegment(){return e.length>0},nextSegment(){return e.shift()??null},flush(){n.flushed++},reset(){n.resets++,e.length=0,t=!1},close(){n.closed=!0}}}export{createFakeVadEngine};
@@ -0,0 +1 @@
1
+ import{_resetVadBackendCache as e,createSherpaVadEngine as t,describeVadBackend as n,hasVadBackend as r,loadVadBackend as i,reportVadBackendOnce as a}from"./backend.js";import{createFakeVadEngine as o}from"./fake-vad.js";import{DEFAULT_VAD_MODEL as s,VAD_MODELS as c,findVadModel as l,requireVadModel as u}from"./model-manifest.js";import{available as d,install as f,installedModels as p,listModels as m,modelCacheDir as h,modelPath as g,resolveModel as _,uninstall as v}from"./models.js";import{inferFormat as y,toFloatSamples as b}from"./pcm.js";import{VoiceActivityDetector as x,createVad as S}from"./vad.js";export{s as DEFAULT_VAD_MODEL,c as VAD_MODELS,x as VoiceActivityDetector,e as _resetVadBackendCache,d as available,o as createFakeVadEngine,t as createSherpaVadEngine,S as createVad,n as describeVadBackend,l as findVadModel,r as hasVadBackend,y as inferFormat,f as install,p as installedModels,m as listModels,i as loadVadBackend,h as modelCacheDir,g as modelPath,a as reportVadBackendOnce,u as requireVadModel,_ as resolveModel,b as toFloatSamples,v as uninstall};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";const e=`silero-v5`,t=`https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models`,n=[{id:`silero-v5`,file:`silero_vad_v5.onnx`,url:`${t}/silero_vad_v5.onnx`,bytes:2313101,sha256:`6b99cbfd39246b6706f98ec13c7c50c6b299181f2474fa05cbc8046acc274396`,license:`MIT`,sampleRate:16e3,windowSize:512,description:`Silero VAD v5 (2.2 MiB) — the default. Takes input/state/sr; 16 kHz, 512-sample windows.`},{id:`silero-v4`,file:`silero_vad.onnx`,url:`${t}/silero_vad.onnx`,bytes:643854,sha256:`9e2449e1087496d8d4caba907f23e0bd3f78d91fa552479bb9c23ac09cbb1fd6`,license:`MIT`,sampleRate:16e3,windowSize:512,description:`Silero VAD v4 (0.6 MiB) — smaller and older. Only its 16 kHz branch is covered here.`}];function findVadModel(t=e){return n.find(e=>e.id===t)}function requireVadModel(t=e){let r=findVadModel(t);if(r===void 0)throw RangeError(`Unknown VAD model '${t}' — known ids: ${n.map(e=>e.id).join(`, `)}`);return r}export{e as DEFAULT_VAD_MODEL,n as VAD_MODELS,findVadModel,requireVadModel};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{hasVadBackend as e}from"./backend.js";import{DEFAULT_VAD_MODEL as t,VAD_MODELS as n,requireVadModel as r}from"./model-manifest.js";import i from"gi://GLib?version=2.0";import a from"gi://Gio?version=2.0";import o from"@gjsify/fetch";function modelCacheDir(){return i.build_filenamev([i.get_user_cache_dir(),`gjsify`,`speech-models`])}function modelPath(e=t){return i.build_filenamev([modelCacheDir(),r(e).file])}const s=new Map;function sizeOf(e){try{return a.File.new_for_path(e).query_info(a.FILE_ATTRIBUTE_STANDARD_SIZE,a.FileQueryInfoFlags.NONE,null).get_size()}catch{return null}}function isCached(e){return sizeOf(i.build_filenamev([modelCacheDir(),e.file]))===e.bytes}function sha256(e){let t=i.Checksum.new(i.ChecksumType.SHA256);t.update(e);let n=t.get_string();if(n===null)throw Error(`GLib.Checksum returned no digest`);return n.toLowerCase()}function available(n=t){let i=r(n);return e()?isCached(i)?`available`:s.has(i.id)?`downloading`:`downloadable`:`unavailable`}function listModels(){return n.map(e=>({entry:e,availability:available(e.id)}))}function installedModels(){return n.filter(isCached).map(e=>e.id)}async function download(e,t){let n=modelCacheDir(),r=i.build_filenamev([n,e.file]),s=await(t.fetchImpl??o)(e.url,{signal:t.signal,redirect:`follow`});if(!s.ok)throw Error(`Downloading ${e.id} from ${e.url} failed: HTTP ${s.status}`);let c=s.headers.get(`content-length`);if(c!==null&&Number(c)!==e.bytes)throw Error(`Downloading ${e.id} refused: ${e.url} declares ${c} bytes, the manifest says ${e.bytes}`);let l=new Uint8Array(await s.arrayBuffer());if(l.length!==e.bytes)throw Error(`Downloading ${e.id} failed: got ${l.length} bytes, expected ${e.bytes}`);let u=sha256(l);if(u!==e.sha256)throw Error(`Downloading ${e.id} failed: sha256 ${u}, expected ${e.sha256}`);i.mkdir_with_parents(n,493);let d=`${r}.part-${i.uuid_string_random()}`,f=a.File.new_for_path(d);try{f.replace_contents(l,null,!1,a.FileCreateFlags.REPLACE_DESTINATION,null),f.move(a.File.new_for_path(r),a.FileCopyFlags.OVERWRITE,null,null)}catch(e){try{f.delete(null)}catch{}throw e}return t.onProgress?.({modelId:e.id,received:e.bytes,total:e.bytes}),r}function install(n=t,a={}){let o;try{o=r(n)}catch(e){return Promise.reject(e)}if(!e())return Promise.reject(Error(`install('${o.id}'): no native VAD engine on this host, so a model would be unusable — gate on available() and show describeVadBackend() instead.`));let c=i.build_filenamev([modelCacheDir(),o.file]);if(!a.force&&isCached(o))return a.onProgress?.({modelId:o.id,received:o.bytes,total:o.bytes}),Promise.resolve(c);let l=s.get(o.id);if(l!==void 0)return l;let u=download(o,a).finally(()=>s.delete(o.id));return s.set(o.id,u),u}function uninstall(e=t){let n=r(e);try{return a.File.new_for_path(i.build_filenamev([modelCacheDir(),n.file])).delete(null)}catch{return!1}}function resolveModel(e=t){if(e.includes(`/`)||e.includes(`\\`))return e;let n=r(e),a=i.build_filenamev([modelCacheDir(),n.file]);if(!isCached(n))throw Error(`VAD model '${n.id}' is not cached (${a}) — await install('${n.id}') first, or pass a path to a model file. ${n.bytes} bytes from ${n.url}.`);return a}export{available,install,installedModels,listModels,modelCacheDir,modelPath,resolveModel,uninstall};
package/lib/esm/pcm.js ADDED
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";function inferFormat(e){return e instanceof Float32Array?`F32LE`:e instanceof Int16Array?`S16LE`:null}const e=1/32768;function toFloatSamples(t,n){if(t instanceof Float32Array)return t;let r=n??inferFormat(t);if(r===null)throw TypeError(`A Uint8Array chunk carries no sample format — pass format: 'S16LE' or 'F32LE' (openPcmSource() reports its own as source.format), or hand over an Int16Array/Float32Array.`);if(t instanceof Int16Array){let n=new Float32Array(t.length);for(let r=0;r<t.length;r++)n[r]=t[r]*e;return n}let i=new DataView(t.buffer,t.byteOffset,t.byteLength);if(r===`F32LE`){let e=i.byteLength>>2,t=new Float32Array(e);for(let n=0;n<e;n++)t[n]=i.getFloat32(n*4,!0);return t}let a=i.byteLength>>1,o=new Float32Array(a);for(let t=0;t<a;t++)o[t]=i.getInt16(t*2,!0)*e;return o}export{inferFormat,toFloatSamples};
package/lib/esm/vad.js ADDED
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{createSherpaVadEngine as e,loadVadBackend as t,reportVadBackendOnce as n}from"./backend.js";import{resolveModel as r}from"./models.js";import{toFloatSamples as i}from"./pcm.js";const a={minSilenceMs:300,minSpeechMs:250,maxSpeechMs:2e4,threshold:.5,sampleRate:16e3,windowSize:512,numThreads:1,bufferSeconds:30,debug:!1};function positive(e,t){if(!Number.isFinite(t)||t<=0)throw RangeError(`VadOptions.${e} must be a positive number, got ${t}`);return t}var VoiceActivityDetector=class{sampleRate;minSilenceMs;minSpeechMs;maxSpeechMs;engine;accepted=0;inSpeech=!1;opened=!1;closed=!1;constructor(t={}){this.sampleRate=positive(`sampleRate`,t.sampleRate??a.sampleRate),this.minSilenceMs=positive(`minSilenceMs`,t.minSilenceMs??a.minSilenceMs),this.minSpeechMs=positive(`minSpeechMs`,t.minSpeechMs??a.minSpeechMs),this.maxSpeechMs=positive(`maxSpeechMs`,t.maxSpeechMs??a.maxSpeechMs),this.engine=t.engine??e({model:r(t.model),threshold:t.threshold??a.threshold,minSilenceSeconds:this.minSilenceMs/1e3,minSpeechSeconds:this.minSpeechMs/1e3,maxSpeechSeconds:this.maxSpeechMs/1e3,sampleRate:this.sampleRate,windowSize:positive(`windowSize`,t.windowSize??a.windowSize),numThreads:positive(`numThreads`,t.numThreads??a.numThreads),bufferSeconds:positive(`bufferSeconds`,t.bufferSeconds??a.bufferSeconds),debug:t.debug??a.debug})}open(){if(this.closed)throw Error(`This VoiceActivityDetector is closed — create another one.`);this.opened||=(this.engine.open(),!0)}get isOpen(){return this.opened&&!this.closed}get speaking(){return this.inSpeech}get position(){return this.accepted/this.sampleRate}get positionSamples(){return this.accepted}accept(e,t){this.open();let n=i(e,t);return this.engine.accept(n),this.accepted+=n.length,this.collect(!1)}flush(){return this.opened?(this.engine.flush(),this.collect(!0)):[]}reset(){this.opened&&(this.engine.reset(),this.accepted=0,this.inSpeech=!1)}close(){this.closed||(this.closed=!0,this.engine.close())}async*run(e){let t=e.sampleRate;if(t!==void 0&&t!==this.sampleRate)throw Error(`This detector was built for ${this.sampleRate} Hz and the source reports ${t} Hz. Resampled audio read at the wrong rate is not an error the detector can see — it just reads as silence. Pass sampleRate: ${t} to createVad(), or ask the source for ${this.sampleRate} Hz.`);let n=e.format;for await(let t of e)for(let e of this.accept(t,n))yield e;for(let e of this.flush())yield e}collect(e){let t=[];for(let e=this.engine.nextSegment();e!==null;e=this.engine.nextSegment())this.inSpeech||t.push({type:`speech-start`,at:e.startSample/this.sampleRate}),t.push(this.toSpeechEnd(e)),this.inSpeech=!1;let n=e?!1:this.engine.isSpeech();return n&&!this.inSpeech?(t.push({type:`speech-start`,at:this.position}),this.inSpeech=!0):!n&&this.inSpeech&&(this.inSpeech=!1),t}toSpeechEnd(e){let t=e.startSample/this.sampleRate,n=e.samples.length/this.sampleRate;return{type:`speech-end`,start:t,end:t+n,duration:n,startSample:e.startSample,samples:e.samples}}};function createVad(e={}){return e.engine===void 0&&n(t()),new VoiceActivityDetector(e)}export{VoiceActivityDetector,createVad};
@@ -0,0 +1,103 @@
1
+ import { type NativeSherpaModule } from '@gjsify/sherpa-native';
2
+ import { type TargetOs } from '@gjsify/utils/core';
3
+ export type VadBackendStatus = 'sherpa' | 'absent' | 'failed';
4
+ export interface VadBackend {
5
+ status: VadBackendStatus;
6
+ /** The GjsifySherpa module, or `null` for `absent`/`failed`. */
7
+ module: NativeSherpaModule | null;
8
+ /** The original load error for `absent`/`failed`; `null` otherwise. */
9
+ error: unknown;
10
+ /** The one-time line the USE site prints; `null` = nothing to say. */
11
+ diagnostic: string | null;
12
+ }
13
+ export interface LoadVadBackendOptions {
14
+ /** Override `loadNativeSherpa()`. Tests only. */
15
+ load?: () => NativeSherpaModule | null;
16
+ /** Override the load error `load` failed with. Tests only. */
17
+ loadError?: () => Error | null;
18
+ /** Override the host OS the probe branches on. Tests only. */
19
+ hostOs?: () => TargetOs | undefined;
20
+ }
21
+ /** Classify a failed load. Exported (underscored) for the specs. */
22
+ export declare function _diagnoseVadLoadError(error: unknown): {
23
+ status: 'absent' | 'failed';
24
+ diagnostic: string | null;
25
+ };
26
+ /**
27
+ * Resolve (once) what detector this host has. Emits NOTHING. Synchronous,
28
+ * because loading a typelib is — `install()` is the asynchronous part, and it
29
+ * is about the model rather than the engine.
30
+ *
31
+ * Passing options after the probe ran THROWS instead of being ignored; call
32
+ * {@link _resetVadBackendCache} first.
33
+ */
34
+ export declare function loadVadBackend(options?: LoadVadBackendOptions): VadBackend;
35
+ /**
36
+ * Whether this host has a native detector. Quiet; a QUERY, not a guarantee —
37
+ * a model still has to be cached, which is what `available()` answers.
38
+ */
39
+ export declare function hasVadBackend(): boolean;
40
+ /** Emit the backend's one-time diagnostic — called by `createVad()`, never by the queries. */
41
+ export declare function reportVadBackendOnce(backend: VadBackend): void;
42
+ /**
43
+ * The detector stack in words, and — when there is none — why and what to
44
+ * install. For a settings panel or a bug report; branch on
45
+ * {@link hasVadBackend} instead.
46
+ */
47
+ export declare function describeVadBackend(): string;
48
+ /** Reset the cached probe and its one-time diagnostic — tests only. */
49
+ export declare function _resetVadBackendCache(): void;
50
+ /** One completed stretch of speech, as the engine hands it over. */
51
+ export interface EngineSegment {
52
+ /** The segment's audio, mono float in [-1, 1]. */
53
+ samples: Float32Array;
54
+ /** Offset of the first sample from the first sample ever accepted. */
55
+ startSample: number;
56
+ }
57
+ /**
58
+ * The detector seam: what `VoiceActivityDetector` needs and nothing else.
59
+ *
60
+ * `isSpeech()` is the LIVE edge and `nextSegment()` a COMPLETED segment, and
61
+ * the two are not the same measurement — see the class doc in `vad.ts` for how
62
+ * far apart they are in practice.
63
+ */
64
+ export interface VadEngine {
65
+ /** Load the model. Idempotent. Throws when the model cannot be opened. */
66
+ open(): void;
67
+ /** Feed mono float samples in [-1, 1]. ANY length; the engine buffers the remainder. */
68
+ accept(samples: Float32Array): void;
69
+ /** Whether the engine is inside speech right now. */
70
+ isSpeech(): boolean;
71
+ /** Whether a completed segment is queued. */
72
+ hasSegment(): boolean;
73
+ /** Take the oldest completed segment, or `null`. */
74
+ nextSegment(): EngineSegment | null;
75
+ /** Close an in-progress segment and queue it — end of input. */
76
+ flush(): void;
77
+ /** Drop every queued segment AND the model state. */
78
+ reset(): void;
79
+ /** Release the model. Idempotent. */
80
+ close(): void;
81
+ }
82
+ /** What {@link createSherpaVadEngine} needs. Resolved values: a real path, seconds, samples. */
83
+ export interface SherpaVadEngineConfig {
84
+ /** Absolute path to a Silero `.onnx` file. */
85
+ model: string;
86
+ threshold: number;
87
+ minSilenceSeconds: number;
88
+ minSpeechSeconds: number;
89
+ maxSpeechSeconds: number;
90
+ sampleRate: number;
91
+ windowSize: number;
92
+ numThreads: number;
93
+ bufferSeconds: number;
94
+ debug: boolean;
95
+ }
96
+ /**
97
+ * A {@link VadEngine} over `GjsifySherpa.Vad`.
98
+ *
99
+ * Construction does not load anything — the GObject's own knobs are construct
100
+ * properties and `open()` is a separate step, so this mirrors that rather than
101
+ * adding a second lifecycle on top.
102
+ */
103
+ export declare function createSherpaVadEngine(config: SherpaVadEngineConfig): VadEngine;
@@ -0,0 +1,23 @@
1
+ import type { VadEngine } from './backend.js';
2
+ export interface FakeVadEngine extends VadEngine {
3
+ /** What `isSpeech()` answers from now on. */
4
+ setSpeech(live: boolean): void;
5
+ /** Queue a completed segment; the next `accept()` drains it. */
6
+ pushSegment(startSample: number, lengthSamples: number): void;
7
+ /** Samples handed to `accept()` so far. */
8
+ readonly accepted: number;
9
+ /** `accept()` calls so far. */
10
+ readonly chunks: number;
11
+ readonly opened: boolean;
12
+ readonly flushed: number;
13
+ readonly resets: number;
14
+ readonly closed: boolean;
15
+ }
16
+ /**
17
+ * An engine that answers exactly what the test told it to.
18
+ *
19
+ * It does NOT simulate Silero: `setSpeech()` and `pushSegment()` are the
20
+ * engine's two observable behaviours, and the point is to drive them into
21
+ * combinations rather than to reproduce how a model would reach them.
22
+ */
23
+ export declare function createFakeVadEngine(): FakeVadEngine;
@@ -0,0 +1,12 @@
1
+ export { createVad, VoiceActivityDetector } from './vad.js';
2
+ export type { SpeechEndEvent, SpeechStartEvent, VadEvent, VadOptions } from './vad.js';
3
+ export { available, install, installedModels, listModels, modelCacheDir, modelPath, resolveModel, uninstall, } from './models.js';
4
+ export type { InstallOptions, InstallProgress, VadModelAvailability } from './models.js';
5
+ export { DEFAULT_VAD_MODEL, findVadModel, requireVadModel, VAD_MODELS } from './model-manifest.js';
6
+ export type { VadModelEntry } from './model-manifest.js';
7
+ export { createSherpaVadEngine, describeVadBackend, hasVadBackend, loadVadBackend, reportVadBackendOnce, _resetVadBackendCache, } from './backend.js';
8
+ export type { EngineSegment, LoadVadBackendOptions, VadBackend, VadBackendStatus, VadEngine } from './backend.js';
9
+ export { inferFormat, toFloatSamples } from './pcm.js';
10
+ export type { PcmChunk, PcmFormat, PcmSourceLike, VadInput } from './pcm.js';
11
+ export { createFakeVadEngine } from './fake-vad.js';
12
+ export type { FakeVadEngine } from './fake-vad.js';
@@ -0,0 +1,32 @@
1
+ /** One fetchable Silero VAD model. */
2
+ export interface VadModelEntry {
3
+ /** Stable id used by `available()`, `install()` and `VadOptions.model`. */
4
+ readonly id: string;
5
+ /** File name inside the cache directory. Upstream's own name, so a hand-placed file matches. */
6
+ readonly file: string;
7
+ /** Where it is fetched from. Upstream only — never a mirror of ours. */
8
+ readonly url: string;
9
+ /** Exact size in bytes. */
10
+ readonly bytes: number;
11
+ /** SHA-256 of the file, lowercase hex. */
12
+ readonly sha256: string;
13
+ /** SPDX id of the MODEL's licence, which is not this package's. */
14
+ readonly license: string;
15
+ /** Sample rate this entry has been measured at. */
16
+ readonly sampleRate: number;
17
+ /** Samples per inference window at {@link sampleRate}. */
18
+ readonly windowSize: number;
19
+ /** One line for a settings panel or a download prompt. */
20
+ readonly description: string;
21
+ }
22
+ /**
23
+ * The default model. v5 is what ADR 0127 decision 10 names, and it is four
24
+ * times the size of v4 — 2.2 MiB, which is small enough that the choice is
25
+ * about accuracy rather than bandwidth.
26
+ */
27
+ export declare const DEFAULT_VAD_MODEL = "silero-v5";
28
+ export declare const VAD_MODELS: readonly VadModelEntry[];
29
+ /** The entry for `id`, or `undefined`. `id` may also be `undefined`, meaning {@link DEFAULT_VAD_MODEL}. */
30
+ export declare function findVadModel(id?: string): VadModelEntry | undefined;
31
+ /** The entry for `id`, or a `RangeError` naming the ids that do exist. */
32
+ export declare function requireVadModel(id?: string): VadModelEntry;
@@ -0,0 +1,71 @@
1
+ import fetch from '@gjsify/fetch';
2
+ import { type VadModelEntry } from './model-manifest.js';
3
+ /**
4
+ * What `available()` answers, as the Web Speech on-device additions spell it:
5
+ *
6
+ * • `available` — the file is cached and verified-by-size; use it now.
7
+ * • `downloading` — an `install()` for this id is in flight in this process.
8
+ * • `downloadable` — the engine is here, the file is not; `install()` fetches it.
9
+ * • `unavailable` — no native engine on this host, so no model helps.
10
+ */
11
+ export type VadModelAvailability = 'available' | 'downloading' | 'downloadable' | 'unavailable';
12
+ export interface InstallProgress {
13
+ readonly modelId: string;
14
+ /** Bytes written so far. Jumps straight to `total` — see the header on buffering. */
15
+ readonly received: number;
16
+ /** The manifest's size for this model. */
17
+ readonly total: number;
18
+ }
19
+ export interface InstallOptions {
20
+ /** Called at least once, with `received === total` on success. */
21
+ onProgress?: (progress: InstallProgress) => void;
22
+ /** Abort the fetch. */
23
+ signal?: AbortSignal;
24
+ /** Override `fetch` from `@gjsify/fetch`. Tests only. */
25
+ fetchImpl?: typeof fetch;
26
+ /** Re-fetch and overwrite a cached file instead of returning it. */
27
+ force?: boolean;
28
+ }
29
+ /** `GLib.get_user_cache_dir()/gjsify/speech-models` — shared with the other ADR 0127 packages. */
30
+ export declare function modelCacheDir(): string;
31
+ /** Where `id` is cached, whether or not it exists yet. */
32
+ export declare function modelPath(id?: string): string;
33
+ /**
34
+ * Whether this host can run a VAD with `id`, and what it would take.
35
+ *
36
+ * QUIET and synchronous. Never downloads, never loads a model, and never
37
+ * throws for a host without an engine — only for an id the manifest does not
38
+ * have, which is a programming error rather than a host condition.
39
+ */
40
+ export declare function available(id?: string): VadModelAvailability;
41
+ /** Every manifest entry with what it would take, for a settings panel. */
42
+ export declare function listModels(): {
43
+ entry: VadModelEntry;
44
+ availability: VadModelAvailability;
45
+ }[];
46
+ /** Cached model ids, in manifest order. */
47
+ export declare function installedModels(): string[];
48
+ /**
49
+ * Make `id` usable, fetching it if it is not cached. Resolves to its path.
50
+ *
51
+ * Idempotent and safe to call concurrently: a cached file returns immediately
52
+ * and two overlapping calls share one download. Rejects when the host has no
53
+ * native engine — `install()` is a commitment to a 2.2 MiB fetch, so it does
54
+ * not quietly succeed into a state where `createVad()` still cannot run.
55
+ *
56
+ * EVERY failure is a rejection, including an unknown id: a function that
57
+ * returns a promise and sometimes throws before it has to be awaited needs two
58
+ * error paths at every call site.
59
+ */
60
+ export declare function install(id?: string, options?: InstallOptions): Promise<string>;
61
+ /** Delete the cached file for `id`. `false` when there was nothing to delete. */
62
+ export declare function uninstall(id?: string): boolean;
63
+ /**
64
+ * Resolve `VadOptions.model` to a path on disk.
65
+ *
66
+ * A value containing a path separator is taken as a path and used as given —
67
+ * that is how a caller points at a model this manifest does not know. Anything
68
+ * else is a manifest id, and it must already be cached: resolving must not
69
+ * start a download behind a synchronous constructor.
70
+ */
71
+ export declare function resolveModel(model?: string): string;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,34 @@
1
+ /** The byte layouts `openPcmSource()` can be asked for. */
2
+ export type PcmFormat = 'S16LE' | 'F32LE';
3
+ /** A chunk of interleaved mono samples. `Uint8Array` is raw bytes in {@link PcmFormat}. */
4
+ export type PcmChunk = Uint8Array | Int16Array | Float32Array;
5
+ /**
6
+ * As much of `@gjsify/media-capture`'s `PcmSource` as this package reads.
7
+ *
8
+ * Both fields are optional so a bare `AsyncIterable` works too — then the
9
+ * sample rate falls back to the detector's own and the format has to be either
10
+ * inferable from the chunk type or given explicitly.
11
+ */
12
+ export interface PcmSourceLike extends AsyncIterable<PcmChunk> {
13
+ readonly sampleRate?: number;
14
+ readonly format?: PcmFormat;
15
+ }
16
+ /** Any source `run()` accepts: a `PcmSource`-like object or a plain async iterable of chunks. */
17
+ export type VadInput = PcmSourceLike | AsyncIterable<PcmChunk>;
18
+ /**
19
+ * The format a chunk carries in its own type, or `null` for raw bytes.
20
+ *
21
+ * `Uint8Array` is deliberately NOT guessed at: `S16LE` and `F32LE` are both
22
+ * plausible for a byte buffer, and guessing wrong does not fail — it produces
23
+ * audio that is silently wrong, which a VAD then reports as silence.
24
+ */
25
+ export declare function inferFormat(chunk: PcmChunk): PcmFormat | null;
26
+ /**
27
+ * Convert `chunk` to mono float samples in [-1, 1].
28
+ *
29
+ * A `Float32Array` is returned as-is — the detector only reads it, and the
30
+ * caller owns the buffer either way. An odd trailing byte in an `S16LE` buffer
31
+ * is dropped rather than read past the end; GStreamer does not split a sample
32
+ * across buffers, so this is a guard for a hand-built chunk.
33
+ */
34
+ export declare function toFloatSamples(chunk: PcmChunk, format?: PcmFormat): Float32Array;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,173 @@
1
+ import { type VadEngine } from './backend.js';
2
+ import { type PcmChunk, type PcmFormat, type VadInput } from './pcm.js';
3
+ /** Speech has begun and is still going. */
4
+ export interface SpeechStartEvent {
5
+ type: 'speech-start';
6
+ /**
7
+ * Stream position where the detector CONFIRMED speech, in seconds.
8
+ *
9
+ * This is the live edge, and it lags the acoustic onset — the detector has
10
+ * to hear {@link VadOptions.minSpeechMs} of speech before it will commit.
11
+ * Measured on two 16 kHz recordings with the defaults: confirmed at 0.67 s
12
+ * and 0.38 s for onsets that the matching `speech-end` later reported at
13
+ * 0.32 s and 0.03 s, so about 0.35 s behind in both.
14
+ *
15
+ * That lag is the right behaviour for a "listening" indicator, which must
16
+ * not flicker on a cough. For trimming audio, use `speech-end.start`,
17
+ * which is the onset itself.
18
+ */
19
+ at: number;
20
+ }
21
+ /** A completed stretch of speech, with its audio. */
22
+ export interface SpeechEndEvent {
23
+ type: 'speech-end';
24
+ /** Onset in seconds from the first sample accepted — back-dated by the detector, not the live edge. */
25
+ start: number;
26
+ /** End in seconds. */
27
+ end: number;
28
+ /** `end - start`. */
29
+ duration: number;
30
+ /** Onset in samples, for placing the audio without going through seconds. */
31
+ startSample: number;
32
+ /** The segment's audio: mono float in [-1, 1] at {@link VadOptions.sampleRate}. */
33
+ samples: Float32Array;
34
+ }
35
+ export type VadEvent = SpeechStartEvent | SpeechEndEvent;
36
+ export interface VadOptions {
37
+ /**
38
+ * A model id from the manifest (`silero-v5`, `silero-v4`) or a path to an
39
+ * `.onnx` file. An id must already be cached — `await install(id)` first.
40
+ * Anything containing a path separator is used as given.
41
+ */
42
+ model?: string;
43
+ /**
44
+ * Silence that ends a stretch of speech, in ms. Default **300**.
45
+ *
46
+ * ADR 0127 decision 10's number, not Silero's 550: the whole wait is added
47
+ * to every reply, and 300 ms is short enough not to feel late while still
48
+ * sitting out the pause inside a sentence.
49
+ */
50
+ minSilenceMs?: number;
51
+ /** Speech shorter than this is discarded as a click or a cough. Default 250 ms. */
52
+ minSpeechMs?: number;
53
+ /** A stretch is cut at this length even without a pause. Default 20 000 ms. */
54
+ maxSpeechMs?: number;
55
+ /** Speech probability above which a window counts as speech. Default 0.5. */
56
+ threshold?: number;
57
+ /** Sample rate of the audio fed to {@link VoiceActivityDetector.accept}. Default 16 000. */
58
+ sampleRate?: number;
59
+ /** Samples per inference window. Default 512, which is Silero at 16 kHz. */
60
+ windowSize?: number;
61
+ /** onnxruntime threads. Default 1 — enough for Silero and the cheapest. */
62
+ numThreads?: number;
63
+ /** The engine's internal ring buffer, in seconds. Default 30; must exceed `maxSpeechMs`. */
64
+ bufferSeconds?: number;
65
+ /** Make sherpa-onnx print its configuration and decisions to stderr. */
66
+ debug?: boolean;
67
+ /** Replace the native engine. Tests use `createFakeVadEngine()`; nothing else should. */
68
+ engine?: VadEngine;
69
+ }
70
+ /**
71
+ * A voice activity detector over one audio stream.
72
+ *
73
+ * Feed it whatever chunks the capture source produced — any length, the engine
74
+ * buffers the remainder — and it answers with the events that fell due:
75
+ *
76
+ * ```ts
77
+ * const vad = createVad({ model: 'silero-v5' });
78
+ * vad.open();
79
+ * for await (const chunk of source) {
80
+ * for (const event of vad.accept(chunk, source.format)) {
81
+ * if (event.type === 'speech-start') showListening();
82
+ * else transcribe(event.samples);
83
+ * }
84
+ * }
85
+ * for (const event of vad.flush()) transcribe((event as SpeechEndEvent).samples);
86
+ * vad.close();
87
+ * ```
88
+ *
89
+ * `flush()` is not optional. The last utterance of a file or a stopped
90
+ * recording is closed by nothing — no silence follows it — so without a flush
91
+ * it is never reported at all.
92
+ */
93
+ export declare class VoiceActivityDetector {
94
+ readonly sampleRate: number;
95
+ readonly minSilenceMs: number;
96
+ readonly minSpeechMs: number;
97
+ readonly maxSpeechMs: number;
98
+ private readonly engine;
99
+ private accepted;
100
+ private inSpeech;
101
+ private opened;
102
+ private closed;
103
+ constructor(options?: VadOptions);
104
+ /** Load the model. Idempotent. Throws when the model file cannot be opened. */
105
+ open(): void;
106
+ get isOpen(): boolean;
107
+ /** Whether the detector is inside speech right now — the live edge, for an indicator. */
108
+ get speaking(): boolean;
109
+ /** Seconds of audio accepted so far: the timeline the events are placed on. */
110
+ get position(): number;
111
+ /** Samples accepted so far. */
112
+ get positionSamples(): number;
113
+ /**
114
+ * Feed one chunk and collect the events it completed.
115
+ *
116
+ * `format` is only consulted for a raw `Uint8Array`; an `Int16Array` or
117
+ * `Float32Array` carries its own. Returns an empty array far more often
118
+ * than not — nothing falls due in most chunks.
119
+ */
120
+ accept(chunk: PcmChunk, format?: PcmFormat): VadEvent[];
121
+ /**
122
+ * Close an in-progress stretch of speech and collect what that completed —
123
+ * end of input.
124
+ *
125
+ * Idempotent and cheap on a detector that is not inside speech. Does not
126
+ * close the detector: after a flush it keeps taking audio, with the
127
+ * timeline running on.
128
+ */
129
+ flush(): VadEvent[];
130
+ /**
131
+ * Drop every queued segment and the model state, and forget the timeline.
132
+ *
133
+ * For reusing one detector across unrelated recordings. A `speech-start`
134
+ * already reported is not retracted — there is no event for that.
135
+ */
136
+ reset(): void;
137
+ /** Release the model. Idempotent. The detector is not reusable afterwards. */
138
+ close(): void;
139
+ /**
140
+ * Drive the detector from a capture source and yield its events.
141
+ *
142
+ * Takes `@gjsify/media-capture`'s `openPcmSource()` — structurally, so
143
+ * neither package depends on the other — or any async iterable of chunks.
144
+ * Flushes at the end of the stream, so the last utterance is reported; the
145
+ * detector is left open, because the caller owns its lifetime.
146
+ */
147
+ run(source: VadInput): AsyncGenerator<VadEvent, void, undefined>;
148
+ /**
149
+ * The state machine, and the reason it is one function.
150
+ *
151
+ * COMPLETED SEGMENTS ARE DRAINED FIRST, then the live edge is read. The
152
+ * order is what makes the three awkward cases come out right:
153
+ *
154
+ * • Speech that began AND ended inside this one chunk never raised a live
155
+ * edge, so the drain emits a synthetic `speech-start` before its
156
+ * `speech-end`. A consumer never sees an end without a start.
157
+ * • A `maxSpeechMs` cut arrives as a queued segment while the live edge
158
+ * is still true, so it reads as `speech-end` immediately followed by a
159
+ * fresh `speech-start` — the stretch continues, under a new start.
160
+ * • A burst too short to keep (< `minSpeechMs`) leaves the live edge and
161
+ * queues nothing. That is a `speech-start` with no `speech-end`, and it
162
+ * stays that way: inventing a zero-length end would make a cough
163
+ * indistinguishable from a word.
164
+ */
165
+ private collect;
166
+ private toSpeechEnd;
167
+ }
168
+ /**
169
+ * Build a detector. Throws when this host has no native engine, or when
170
+ * `options.model` is a manifest id that is not cached yet — gate on
171
+ * `hasVadBackend()` and `available()`.
172
+ */
173
+ export declare function createVad(options?: VadOptions): VoiceActivityDetector;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
package/package.json CHANGED
@@ -1,6 +1,80 @@
1
1
  {
2
- "name": "@gjsify/vad",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@gjsify/vad",
3
+ "version": "0.64.16",
4
+ "description": "Voice activity detection for GJS: Silero on sherpa-onnx, reporting when speech starts and when a stretch of speech has ended after 300 ms of silence. Takes PCM from @gjsify/media-capture or any async iterable of chunks, and downloads its model on first use instead of bundling one.",
5
+ "type": "module",
6
+ "module": "lib/esm/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/index.d.ts",
11
+ "default": "./lib/esm/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "lib"
16
+ ],
17
+ "sideEffects": false,
18
+ "scripts": {
19
+ "clear": "gjsify clear lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs",
20
+ "check": "gjsify tsc --noEmit",
21
+ "build": "gjsify run build:gjsify && gjsify run build:types",
22
+ "build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test*.{mts,ts}'",
23
+ "build:types": "gjsify tsc",
24
+ "build:test": "gjsify run build:test:gjs",
25
+ "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
26
+ "test": "gjsify run build:gjsify && gjsify run build:test && gjsify run test:gjs",
27
+ "test:gjs": "gjsify run test.gjs.mjs"
28
+ },
29
+ "keywords": [
30
+ "gjs",
31
+ "vad",
32
+ "voice-activity-detection",
33
+ "silero",
34
+ "sherpa-onnx",
35
+ "speech"
36
+ ],
37
+ "dependencies": {
38
+ "@gjsify/fetch": "^0.64.16",
39
+ "@gjsify/sherpa-native": "^0.64.16",
40
+ "@gjsify/utils": "^0.64.16"
41
+ },
42
+ "devDependencies": {
43
+ "@girs/gio-2.0": "5.4.0",
44
+ "@girs/gjs": "5.4.0",
45
+ "@girs/glib-2.0": "5.4.0",
46
+ "@gjsify/cli": "^0.64.16",
47
+ "@gjsify/unit": "^0.64.16",
48
+ "@types/node": "^25.9.2",
49
+ "typescript": "^6.0.3"
50
+ },
51
+ "gjsify": {
52
+ "runtimes": {
53
+ "gjs": "polyfill",
54
+ "node": "none",
55
+ "browser": "none",
56
+ "nativescript": "none"
57
+ },
58
+ "os": {
59
+ "linux": "partial",
60
+ "darwin": "none",
61
+ "win32": "none"
62
+ },
63
+ "osNotes": {
64
+ "linux": "Needs the @gjsify/sherpa-native prebuild, which exists for linux-x64 only today. Without it available() answers 'unavailable' and createVad() throws with the package to install; hasVadBackend() is the quiet check.",
65
+ "darwin": "@gjsify/sherpa-native is not built for macOS yet, so there is no detector to drive. available() answers 'unavailable'.",
66
+ "win32": "@gjsify/sherpa-native is not built for Windows yet, so there is no detector to drive. available() answers 'unavailable'."
67
+ },
68
+ "tier": 3
69
+ },
70
+ "license": "MIT",
71
+ "repository": {
72
+ "type": "git",
73
+ "url": "git+https://github.com/gjsify/gjsify.git",
74
+ "directory": "packages/web/vad"
75
+ },
76
+ "bugs": {
77
+ "url": "https://github.com/gjsify/gjsify/issues"
78
+ },
79
+ "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/web/vad#readme"
80
+ }