@untemps/vocal 2.0.0-beta.20 → 2.0.0-beta.22

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/CHANGELOG.md CHANGED
@@ -1,3 +1,52 @@
1
+ # [2.0.0-beta.22](https://github.com/untemps/vocal/compare/v2.0.0-beta.21...v2.0.0-beta.22) (2026-05-22)
2
+
3
+
4
+ ### Code Refactoring
5
+
6
+ * Move from class-based to functional API ([#88](https://github.com/untemps/vocal/issues/88)) ([1161ce8](https://github.com/untemps/vocal/commit/1161ce8ce983a7f216b5a11803bbc5ad90a68dc5))
7
+
8
+
9
+ ### BREAKING CHANGES
10
+
11
+ * every public entry point changes shape:
12
+ - `new Vocal(options)` → `createVocal(options)`
13
+ - `Vocal.isSupported` (static getter) → `isSupported()` (function)
14
+ - `Vocal.eventTypes` (static) → `eventTypes` (named export)
15
+ - `vocal.addEventListener(type, cb)` → `vocal.on(type, cb)`
16
+ - `vocal.removeEventListener(type, cb?)` → `vocal.off(type, cb?)`
17
+ - Side-effect methods (`stop`, `abort`, `on`, `off`, `cleanup`) now
18
+ return `void` instead of `this` — chaining is no longer supported.
19
+ - The `Vocal` class is no longer exported; the new `VocalInstance`
20
+ interface describes the object returned by `createVocal()`.
21
+ Migration:
22
+ // before
23
+ import { Vocal } from '@untemps/vocal'
24
+ if (!Vocal.isSupported) throw new Error()
25
+ const vocal = new Vocal({ lang: 'fr-FR' })
26
+ vocal.addEventListener('result', cb)
27
+ // after
28
+ import { createVocal, isSupported } from '@untemps/vocal'
29
+ if (!isSupported()) throw new Error()
30
+ const vocal = createVocal({ lang: 'fr-FR' })
31
+ vocal.on('result', cb)
32
+
33
+ # [2.0.0-beta.21](https://github.com/untemps/vocal/compare/v2.0.0-beta.20...v2.0.0-beta.21) (2026-05-20)
34
+
35
+
36
+ ### Code Refactoring
37
+
38
+ * Remove once() method ([#87](https://github.com/untemps/vocal/issues/87)) ([3748768](https://github.com/untemps/vocal/commit/3748768c33f61ba170f4ffd09ccba23063e5c8c5))
39
+
40
+
41
+ ### BREAKING CHANGES
42
+
43
+ * vocal.once(eventType, callback) is removed. Consumers relying on it must replace the call with a manual addEventListener + removeEventListener pair:
44
+ const handler = (event, best) => {
45
+ vocal.removeEventListener('result', handler)
46
+ // ...
47
+ }
48
+ vocal.addEventListener('result', handler)
49
+
1
50
  # [2.0.0-beta.20](https://github.com/untemps/vocal/compare/v2.0.0-beta.19...v2.0.0-beta.20) (2026-05-20)
2
51
 
3
52
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @untemps/vocal
2
2
 
3
- Class wrapped around the SpeechRecognition Web API
3
+ Functional wrapper around the SpeechRecognition Web API
4
4
 
5
5
  ![npm](https://img.shields.io/npm/v/@untemps/vocal?style=for-the-badge)
6
6
  ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/untemps/vocal/publish.yml?style=for-the-badge)
@@ -14,27 +14,22 @@ yarn add @untemps/vocal
14
14
 
15
15
  ## Basic Usage
16
16
 
17
- Import `Vocal` to a file.
18
-
19
17
  ```javascript
20
- import { Vocal } from '@untemps/vocal'
18
+ import { createVocal, isSupported } from '@untemps/vocal'
21
19
 
22
20
  // Check whether SpeechRecognition, Permissions and MediaDevices interfaces are supported
23
- if (!Vocal.isSupported) {
21
+ if (!isSupported()) {
24
22
  throw new Error('Vocal is not supported')
25
23
  }
26
24
 
27
25
  // Create a Vocal instance (see below for all available option properties)
28
- const options = {
29
- lang: 'fr-FR',
30
- }
31
- const vocal = new Vocal(options)
26
+ const vocal = createVocal({ lang: 'fr-FR' })
32
27
 
33
- // Subscribe to Vocal instance events (see below for all available events)
34
- vocal.addEventListener('speechstart', (event) => console.log('Vocal starts recording'))
35
- vocal.addEventListener('speechend', (event) => console.log('Vocal stops recording'))
36
- vocal.addEventListener('result', (event, bestAlternative, alternatives) => console.log('Vocal catches a result:', bestAlternative, alternatives))
37
- vocal.addEventListener('error', (event) => console.error(event.error, event.message))
28
+ // Subscribe to instance events (see below for all available events)
29
+ vocal.on('speechstart', (event) => console.log('Vocal starts recording'))
30
+ vocal.on('speechend', (event) => console.log('Vocal stops recording'))
31
+ vocal.on('result', (event, bestAlternative, alternatives) => console.log('Vocal catches a result:', bestAlternative, alternatives))
32
+ vocal.on('error', (event) => console.error(event.error, event.message))
38
33
 
39
34
  // Start recording — rejects on error
40
35
  try {
@@ -49,7 +44,7 @@ vocal.stop()
49
44
  // Abort recording entirely
50
45
  vocal.abort()
51
46
 
52
- // Remove all attached listeners and delete the Vocal instance
47
+ // Remove all attached listeners and release the internal SpeechRecognition instance
53
48
  vocal.cleanup()
54
49
  ```
55
50
 
@@ -97,17 +92,33 @@ Please refer to [this section](https://developer.mozilla.org/en-US/docs/Web/API/
97
92
  | speechstart | Fired when sound recognized by the recognition service as speech has been detected |
98
93
  | start | fired when the recognition service has begun listening to incoming audio |
99
94
 
100
- ## Getters
95
+ For convenience, `eventTypes` is exported as a constant map so consumers can reference type strings symbolically:
96
+
97
+ ```js
98
+ import { eventTypes } from '@untemps/vocal'
99
+ vocal.on(eventTypes.RESULT, handler)
100
+ ```
101
+
102
+ ## Top-level exports
103
+
104
+ | Export | Kind | Description |
105
+ | ------------- | -------- | -------------------------------------------------------------------------------------------------------------------- |
106
+ | `createVocal` | function | Factory that returns a `VocalInstance`. See [Methods](#methods). |
107
+ | `isSupported` | function | Returns `true` if the current environment supports the SpeechRecognition Web API. Call it (it is **not** a getter). |
108
+ | `eventTypes` | const | Map of valid event type strings (e.g. `eventTypes.RESULT === 'result'`). |
109
+
110
+ ## Instance getter
101
111
 
102
- | Getter | Type | Description |
103
- | ----------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------- |
104
- | isSupported | boolean | Whether the current environment supports the SpeechRecognition Web API (static) |
105
- | isRecording | boolean | Whether recognition is currently active — `true` after `start()`, `false` after `stop()`, `abort()`, or `end` event |
112
+ | Getter | Type | Description |
113
+ | ----------- | --------- | -------------------------------------------------------------------------------------------------------------------- |
114
+ | isRecording | boolean | Whether recognition is currently active `true` after `start()`, `false` after `stop()`, `abort()`, or `end` event |
106
115
 
107
116
  ## Methods
108
117
 
109
118
  ### `start({ signal? })`
110
119
 
120
+ Starts recognition. Resolves once the engine is active. Rejects if microphone permission cannot be obtained.
121
+
111
122
  | Parameter | Type | Default | Description |
112
123
  | --------- | ------------- | ----------- | ----------------------------------------------------------------------------- |
113
124
  | signal | AbortSignal | `undefined` | Cancels the in-flight microphone permission request when the signal is aborted |
@@ -122,13 +133,13 @@ controller.abort()
122
133
 
123
134
  ### `stop()`
124
135
 
125
- Stops recognition gracefully, allowing the current audio to be processed before disconnecting. Sets `isRecording` to `false`.
136
+ Stops recognition gracefully, allowing the current audio to be processed before disconnecting. Sets `isRecording` to `false`. In continuous mode, emits the aggregated `result` event just before `end`.
126
137
 
127
138
  ### `abort()`
128
139
 
129
- Stops recognition immediately without processing pending audio. Sets `isRecording` to `false`.
140
+ Stops recognition immediately without processing pending audio. Sets `isRecording` to `false`. Discards any aggregated transcript without emitting.
130
141
 
131
- ### `addEventListener(eventType, callback)`
142
+ ### `on(eventType, callback)`
132
143
 
133
144
  Registers a callback for the given event type. Multiple callbacks can be registered for the same type — they stack and all fire in registration order.
134
145
 
@@ -139,7 +150,7 @@ Registers a callback for the given event type. Multiple callbacks can be registe
139
150
 
140
151
  Throws if `eventType` is not a valid `EventType`.
141
152
 
142
- ### `removeEventListener(eventType, callback?)`
153
+ ### `off(eventType, callback?)`
143
154
 
144
155
  Removes a listener for the given event type.
145
156
 
@@ -150,23 +161,26 @@ Removes a listener for the given event type.
150
161
 
151
162
  Throws if `eventType` is not a valid `EventType`.
152
163
 
153
- ### `once(eventType, callback)`
164
+ ### `cleanup()`
154
165
 
155
- Registers a one-shot listener that automatically unregisters itself after firing once.
166
+ Stops recognition, removes all registered listeners, and releases the internal `SpeechRecognition` instance. The returned `VocalInstance` cannot be reused after `cleanup()`.
156
167
 
157
- | Parameter | Type | Description |
158
- | --------- | ------------------------------------------------- | ------------------------------------------ |
159
- | eventType | `EventType` | One of the valid event type strings |
160
- | callback | `ResultEventHandler \| ErrorEventHandler \| GenericEventHandler` | Callback invoked once when the event fires |
168
+ ## Migration from the class-based API (v1.x)
161
169
 
162
170
  ```js
163
- vocal.once('result', (event, bestAlternative, alternatives) => {
164
- console.log(bestAlternative)
165
- vocal.stop()
166
- })
171
+ // Before
172
+ import { Vocal } from '@untemps/vocal'
173
+ if (!Vocal.isSupported) throw new Error()
174
+ const vocal = new Vocal({ lang: 'fr-FR' })
175
+ vocal.addEventListener('result', cb)
176
+ vocal.removeEventListener('result', cb)
177
+
178
+ // After
179
+ import { createVocal, isSupported } from '@untemps/vocal'
180
+ if (!isSupported()) throw new Error()
181
+ const vocal = createVocal({ lang: 'fr-FR' })
182
+ vocal.on('result', cb)
183
+ vocal.off('result', cb)
167
184
  ```
168
185
 
169
- ### `cleanup()`
170
-
171
- Stops recognition, removes all registered listeners, and releases the internal `SpeechRecognition` instance. The `Vocal` object cannot be reused after `cleanup()`.
172
-
186
+ Side-effect methods (`stop`, `abort`, `on`, `off`, `cleanup`) now return `void` — method chaining is no longer supported. `Vocal.eventTypes` is now exported as the top-level `eventTypes` const.
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@untemps/user-permissions-utils`);var t=1e3,n=new Set([`not-allowed`,`service-not-allowed`,`audio-capture`]),r=class r{static defaultOptions={grammars:null,lang:`en-US`,continuous:!1,interimResults:!1,maxAlternatives:1};static eventTypes={AUDIO_END:`audioend`,AUDIO_START:`audiostart`,END:`end`,ERROR:`error`,NO_MATCH:`nomatch`,RESULT:`result`,SOUND_END:`soundend`,SOUND_START:`soundstart`,SPEECH_END:`speechend`,SPEECH_START:`speechstart`,START:`start`};static get isSupported(){return!!r._resolveSpeechRecognition()&&!!(0,e.isNavigatorPermissionsSupported)()&&!!(0,e.isNavigatorMediaDevicesSupported)()}static set isSupported(e){throw Error(`You cannot set isSupported directly.`)}_instance=null;_listeners={};_isRecording=!1;_explicitStop=!1;_lastStartedAt=0;_restartTimeoutId=null;_isRestarting=!1;_finalTranscripts=[];_onEnd=e=>{if(this._shouldAutoRestart()){let n=Math.max(0,t-(Date.now()-this._lastStartedAt));this._isRestarting=!0,this._restartTimeoutId=setTimeout(()=>this._restart(),n),e.stopImmediatePropagation();return}this._isRecording=!1};_onStart=e=>{this._isRestarting&&(e.stopImmediatePropagation(),queueMicrotask(()=>{this._isRestarting=!1}))};_onError=e=>{n.has(e.error)&&(this._explicitStop=!0,this._clearRestartTimeout(),this._isRecording=!1)};_onResult=e=>{let t=e,n=t.results?.[t.resultIndex];n?.isFinal&&this._finalTranscripts.push(r._pickBestAlternative(Array.from(n)).transcript)};constructor(e){let t=r._resolveSpeechRecognition();if(!t)throw new DOMException(`SpeechRecognition not supported`,`NOT_SUPPORTED_ERR`);this._instance=new t;let{grammars:n,...i}={...r.defaultOptions,...e??{}},a=this._instance;if(Object.assign(a,i),n)a.grammars=n;else{let e=r._resolveSpeechGrammarList();a.grammars=e?new e:null}this._instance.addEventListener(r.eventTypes.END,this._onEnd),this._instance.addEventListener(r.eventTypes.START,this._onStart),this._instance.addEventListener(r.eventTypes.ERROR,this._onError),this._instance.addEventListener(r.eventTypes.RESULT,this._onResult)}get isRecording(){return this._isRecording}set isRecording(e){throw Error(`You cannot set isRecording directly.`)}async start({signal:t}={}){if(this._instance)try{if(!await(0,e.getUserMediaStream)(`microphone`,{audio:!0},{signal:t}))throw Error(`Unable to retrieve the stream from media device`);this._explicitStop=!1,this._finalTranscripts=[],this._instance.start(),this._isRecording=!0,this._lastStartedAt=Date.now()}catch(e){if(e instanceof Error&&e.name===`AbortError`)return this;throw e}return this}stop(){return this._instance&&(this._explicitStop=!0,this._clearRestartTimeout(),this._emitAggregatedResult(),this._instance.stop(),this._isRecording=!1),this}abort(){return this._instance&&(this._explicitStop=!0,this._clearRestartTimeout(),this._instance.abort(),this._isRecording=!1,this._finalTranscripts=[]),this}addEventListener(e,t){if(!this._includesEventType(e))throw Error(this._unknownEventTypeMessage(e));if(this._instance){let n=n=>{if(this._isRestarting&&(e===r.eventTypes.END||e===r.eventTypes.START))return;let i=[];if(e===r.eventTypes.RESULT){let e=n;if(e.results?.length>0&&e.resultIndex<e.results.length){let t=Array.from(e.results[e.resultIndex]);i.push(r._pickBestAlternative(t).transcript,t.map(e=>e.transcript))}}t.call(this,n,...i)};this._instance.addEventListener(e,n),this._listeners[e]||(this._listeners[e]=[]),this._listeners[e].push({callback:t,handler:n})}return this}removeEventListener(e,t){if(!this._includesEventType(e))throw Error(this._unknownEventTypeMessage(e));let n=this._instance;if(n&&this._listeners[e])if(t!==void 0){let r=this._listeners[e].findIndex(e=>e.callback===t);r!==-1&&(n.removeEventListener(e,this._listeners[e][r].handler),this._listeners[e].splice(r,1),this._listeners[e].length===0&&delete this._listeners[e])}else this._listeners[e].forEach(({handler:t})=>n.removeEventListener(e,t)),delete this._listeners[e];return this}once(e,t){let n=(...r)=>{t.call(this,...r),this.removeEventListener(e,n)};return this.addEventListener(e,n)}cleanup(){return this.stop(),Object.keys(this._listeners).forEach(e=>this.removeEventListener(e)),this._instance?.removeEventListener(r.eventTypes.END,this._onEnd),this._instance?.removeEventListener(r.eventTypes.START,this._onStart),this._instance?.removeEventListener(r.eventTypes.ERROR,this._onError),this._instance?.removeEventListener(r.eventTypes.RESULT,this._onResult),this._instance=null,this}_restart=()=>{this._restartTimeoutId=null;try{this._instance.start(),this._lastStartedAt=Date.now()}catch{this._isRestarting=!1,this._isRecording=!1}};_emitAggregatedResult(){let e=this._finalTranscripts;if(this._finalTranscripts=[],e.length===0)return;let t=e.join(` `).trim(),n=Object.assign([{transcript:t,confidence:1}],{isFinal:!0}),i=Object.assign(new Event(r.eventTypes.RESULT),{resultIndex:0,results:[n]});[...this._listeners[r.eventTypes.RESULT]??[]].forEach(({handler:e})=>e(i))}static _pickBestAlternative(e){return e.reduce((e,t)=>(t.confidence??0)>(e.confidence??0)?t:e)}_shouldAutoRestart(){return!!this._instance&&!this._explicitStop&&this._instance.continuous}_clearRestartTimeout(){this._restartTimeoutId!==null&&(clearTimeout(this._restartTimeoutId),this._restartTimeoutId=null),this._isRestarting=!1}_includesEventType(e){return Object.values(r.eventTypes).includes(e)}_unknownEventTypeMessage(e){return`Unknown event type "${e}". Valid types are: ${Object.values(r.eventTypes).join(`, `)}.`}static _resolveSpeechRecognition(){if(!(typeof window>`u`))return window.SpeechRecognition??window.webkitSpeechRecognition??window.mozSpeechRecognition??window.msSpeechRecognition}static _resolveSpeechGrammarList(){return window.SpeechGrammarList??window.webkitSpeechGrammarList??window.mozSpeechGrammarList??window.msSpeechGrammarList}};exports.Vocal=r;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@untemps/user-permissions-utils`);var t={AUDIO_END:`audioend`,AUDIO_START:`audiostart`,END:`end`,ERROR:`error`,NO_MATCH:`nomatch`,RESULT:`result`,SOUND_END:`soundend`,SOUND_START:`soundstart`,SPEECH_END:`speechend`,SPEECH_START:`speechstart`,START:`start`},n=1e3,r=new Set([`not-allowed`,`service-not-allowed`,`audio-capture`]),i={grammars:null,lang:`en-US`,continuous:!1,interimResults:!1,maxAlternatives:1},a=()=>{if(!(typeof window>`u`))return window.SpeechRecognition??window.webkitSpeechRecognition??window.mozSpeechRecognition??window.msSpeechRecognition},o=()=>window.SpeechGrammarList??window.webkitSpeechGrammarList??window.mozSpeechGrammarList??window.msSpeechGrammarList,s=e=>e.reduce((e,t)=>(t.confidence??0)>(e.confidence??0)?t:e),c=e=>Object.values(t).includes(e),l=e=>`Unknown event type "${e}". Valid types are: ${Object.values(t).join(`, `)}.`,u=()=>!!a()&&!!(0,e.isNavigatorPermissionsSupported)()&&!!(0,e.isNavigatorMediaDevicesSupported)(),d=u=>{let d=a();if(!d)throw new DOMException(`SpeechRecognition not supported`,`NOT_SUPPORTED_ERR`);let f=new d,p={},m=!1,h=!1,g=0,_=null,v=!1,y=[],b={...i,...u??{}};if(f.lang=b.lang,f.continuous=b.continuous,f.interimResults=b.interimResults,f.maxAlternatives=b.maxAlternatives,b.grammars)f.grammars=b.grammars;else{let e=o();f.grammars=e?new e:null}let x=()=>{_!==null&&(clearTimeout(_),_=null),v=!1},S=()=>!!f&&!h&&f.continuous,C=()=>{_=null;try{f.start(),g=Date.now()}catch{v=!1,m=!1}},w=()=>{let e=y;if(y=[],e.length===0||!p[t.RESULT]?.length)return;let n=e.join(` `).trim(),r=Object.assign([{transcript:n,confidence:1}],{isFinal:!0}),i=Object.assign(new Event(t.RESULT),{resultIndex:0,results:[r]});[...p[t.RESULT]].forEach(({handler:e})=>e(i))},T=[[t.END,e=>{if(S()){let t=Math.max(0,n-(Date.now()-g));v=!0,_=setTimeout(C,t),e.stopImmediatePropagation();return}m=!1}],[t.START,e=>{v&&(e.stopImmediatePropagation(),queueMicrotask(()=>{v=!1}))}],[t.ERROR,e=>{r.has(e.error)&&(h=!0,x(),m=!1)}],[t.RESULT,e=>{let t=e,n=t.results?.[t.resultIndex];n?.isFinal&&y.push(s(Array.from(n)).transcript)}]];T.forEach(([e,t])=>f.addEventListener(e,t));let E=async({signal:t}={})=>{if(f)try{let n=await(0,e.getUserMediaStream)(`microphone`,{audio:!0},{signal:t});if(t?.aborted)return;if(!n)throw Error(`Unable to retrieve the stream from media device`);h=!1,y=[],f.start(),m=!0,g=Date.now()}catch(e){if(e instanceof Error&&e.name===`AbortError`)return;throw e}},D=()=>{f&&(h=!0,x(),w(),f.stop(),m=!1)},O=()=>{f&&(h=!0,x(),f.abort(),m=!1,y=[])},k=(e,n)=>{if(!c(e))throw Error(l(e));if(!f)return;let r=r=>{if(v&&(e===t.END||e===t.START))return;if(e!==t.RESULT){n(r);return}let i=r;if(!(i.results?.length>0)||i.resultIndex>=i.results.length){n(r);return}let a=Array.from(i.results[i.resultIndex]);n(r,s(a).transcript,a.map(e=>e.transcript))};f.addEventListener(e,r),p[e]||(p[e]=[]),p[e].push({callback:n,handler:r})},A=(e,t)=>{if(!c(e))throw Error(l(e));if(!(!f||!p[e]))if(t!==void 0){let n=p[e].findIndex(e=>e.callback===t);n!==-1&&(f.removeEventListener(e,p[e][n].handler),p[e].splice(n,1),p[e].length===0&&delete p[e])}else p[e].forEach(({handler:t})=>f.removeEventListener(e,t)),delete p[e]};return{get isRecording(){return m},start:E,stop:D,abort:O,on:k,off:A,cleanup:()=>{D(),Object.keys(p).forEach(e=>A(e)),T.forEach(([e,t])=>f?.removeEventListener(e,t)),f=null}}};exports.createVocal=d,exports.eventTypes=t,exports.isSupported=u;
2
2
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { default as Vocal } from './Vocal';
2
- export type { VocalOptions, EventType, ResultEventHandler, ErrorEventHandler, GenericEventHandler, EventHandlerFor, } from './Vocal';
1
+ export { createVocal, isSupported, eventTypes } from './Vocal';
2
+ export type { VocalOptions, VocalInstance, EventType, ResultEventHandler, ErrorEventHandler, GenericEventHandler, EventHandlerFor, } from './Vocal';
package/dist/index.es.js CHANGED
@@ -1,184 +1,141 @@
1
1
  import { getUserMediaStream as e, isNavigatorMediaDevicesSupported as t, isNavigatorPermissionsSupported as n } from "@untemps/user-permissions-utils";
2
2
  //#region src/Vocal.ts
3
- var r = 1e3, i = new Set([
3
+ var r = {
4
+ AUDIO_END: "audioend",
5
+ AUDIO_START: "audiostart",
6
+ END: "end",
7
+ ERROR: "error",
8
+ NO_MATCH: "nomatch",
9
+ RESULT: "result",
10
+ SOUND_END: "soundend",
11
+ SOUND_START: "soundstart",
12
+ SPEECH_END: "speechend",
13
+ SPEECH_START: "speechstart",
14
+ START: "start"
15
+ }, i = 1e3, a = new Set([
4
16
  "not-allowed",
5
17
  "service-not-allowed",
6
18
  "audio-capture"
7
- ]), a = class a {
8
- static defaultOptions = {
9
- grammars: null,
10
- lang: "en-US",
11
- continuous: !1,
12
- interimResults: !1,
13
- maxAlternatives: 1
19
+ ]), o = {
20
+ grammars: null,
21
+ lang: "en-US",
22
+ continuous: !1,
23
+ interimResults: !1,
24
+ maxAlternatives: 1
25
+ }, s = () => {
26
+ if (!(typeof window > "u")) return window.SpeechRecognition ?? window.webkitSpeechRecognition ?? window.mozSpeechRecognition ?? window.msSpeechRecognition;
27
+ }, c = () => window.SpeechGrammarList ?? window.webkitSpeechGrammarList ?? window.mozSpeechGrammarList ?? window.msSpeechGrammarList, l = (e) => e.reduce((e, t) => (t.confidence ?? 0) > (e.confidence ?? 0) ? t : e), u = (e) => Object.values(r).includes(e), d = (e) => `Unknown event type "${e}". Valid types are: ${Object.values(r).join(", ")}.`, f = () => !!s() && !!n() && !!t(), p = (t) => {
28
+ let n = s();
29
+ if (!n) throw new DOMException("SpeechRecognition not supported", "NOT_SUPPORTED_ERR");
30
+ let f = new n(), p = {}, m = !1, h = !1, g = 0, _ = null, v = !1, y = [], b = {
31
+ ...o,
32
+ ...t ?? {}
14
33
  };
15
- static eventTypes = {
16
- AUDIO_END: "audioend",
17
- AUDIO_START: "audiostart",
18
- END: "end",
19
- ERROR: "error",
20
- NO_MATCH: "nomatch",
21
- RESULT: "result",
22
- SOUND_END: "soundend",
23
- SOUND_START: "soundstart",
24
- SPEECH_END: "speechend",
25
- SPEECH_START: "speechstart",
26
- START: "start"
27
- };
28
- static get isSupported() {
29
- return !!a._resolveSpeechRecognition() && !!n() && !!t();
30
- }
31
- static set isSupported(e) {
32
- throw Error("You cannot set isSupported directly.");
33
- }
34
- _instance = null;
35
- _listeners = {};
36
- _isRecording = !1;
37
- _explicitStop = !1;
38
- _lastStartedAt = 0;
39
- _restartTimeoutId = null;
40
- _isRestarting = !1;
41
- _finalTranscripts = [];
42
- _onEnd = (e) => {
43
- if (this._shouldAutoRestart()) {
44
- let t = Math.max(0, r - (Date.now() - this._lastStartedAt));
45
- this._isRestarting = !0, this._restartTimeoutId = setTimeout(() => this._restart(), t), e.stopImmediatePropagation();
46
- return;
47
- }
48
- this._isRecording = !1;
49
- };
50
- _onStart = (e) => {
51
- this._isRestarting && (e.stopImmediatePropagation(), queueMicrotask(() => {
52
- this._isRestarting = !1;
53
- }));
54
- };
55
- _onError = (e) => {
56
- i.has(e.error) && (this._explicitStop = !0, this._clearRestartTimeout(), this._isRecording = !1);
57
- };
58
- _onResult = (e) => {
59
- let t = e, n = t.results?.[t.resultIndex];
60
- n?.isFinal && this._finalTranscripts.push(a._pickBestAlternative(Array.from(n)).transcript);
61
- };
62
- constructor(e) {
63
- let t = a._resolveSpeechRecognition();
64
- if (!t) throw new DOMException("SpeechRecognition not supported", "NOT_SUPPORTED_ERR");
65
- this._instance = new t();
66
- let { grammars: n, ...r } = {
67
- ...a.defaultOptions,
68
- ...e ?? {}
69
- }, i = this._instance;
70
- if (Object.assign(i, r), n) i.grammars = n;
71
- else {
72
- let e = a._resolveSpeechGrammarList();
73
- i.grammars = e ? new e() : null;
74
- }
75
- this._instance.addEventListener(a.eventTypes.END, this._onEnd), this._instance.addEventListener(a.eventTypes.START, this._onStart), this._instance.addEventListener(a.eventTypes.ERROR, this._onError), this._instance.addEventListener(a.eventTypes.RESULT, this._onResult);
76
- }
77
- get isRecording() {
78
- return this._isRecording;
79
- }
80
- set isRecording(e) {
81
- throw Error("You cannot set isRecording directly.");
82
- }
83
- async start({ signal: t } = {}) {
84
- if (this._instance) try {
85
- if (!await e("microphone", { audio: !0 }, { signal: t })) throw Error("Unable to retrieve the stream from media device");
86
- this._explicitStop = !1, this._finalTranscripts = [], this._instance.start(), this._isRecording = !0, this._lastStartedAt = Date.now();
87
- } catch (e) {
88
- if (e instanceof Error && e.name === "AbortError") return this;
89
- throw e;
90
- }
91
- return this;
92
- }
93
- stop() {
94
- return this._instance && (this._explicitStop = !0, this._clearRestartTimeout(), this._emitAggregatedResult(), this._instance.stop(), this._isRecording = !1), this;
95
- }
96
- abort() {
97
- return this._instance && (this._explicitStop = !0, this._clearRestartTimeout(), this._instance.abort(), this._isRecording = !1, this._finalTranscripts = []), this;
98
- }
99
- addEventListener(e, t) {
100
- if (!this._includesEventType(e)) throw Error(this._unknownEventTypeMessage(e));
101
- if (this._instance) {
102
- let n = (n) => {
103
- if (this._isRestarting && (e === a.eventTypes.END || e === a.eventTypes.START)) return;
104
- let r = [];
105
- if (e === a.eventTypes.RESULT) {
106
- let e = n;
107
- if (e.results?.length > 0 && e.resultIndex < e.results.length) {
108
- let t = Array.from(e.results[e.resultIndex]);
109
- r.push(a._pickBestAlternative(t).transcript, t.map((e) => e.transcript));
110
- }
111
- }
112
- t.call(this, n, ...r);
113
- };
114
- this._instance.addEventListener(e, n), this._listeners[e] || (this._listeners[e] = []), this._listeners[e].push({
115
- callback: t,
116
- handler: n
117
- });
118
- }
119
- return this;
120
- }
121
- removeEventListener(e, t) {
122
- if (!this._includesEventType(e)) throw Error(this._unknownEventTypeMessage(e));
123
- let n = this._instance;
124
- if (n && this._listeners[e]) if (t !== void 0) {
125
- let r = this._listeners[e].findIndex((e) => e.callback === t);
126
- r !== -1 && (n.removeEventListener(e, this._listeners[e][r].handler), this._listeners[e].splice(r, 1), this._listeners[e].length === 0 && delete this._listeners[e]);
127
- } else this._listeners[e].forEach(({ handler: t }) => n.removeEventListener(e, t)), delete this._listeners[e];
128
- return this;
129
- }
130
- once(e, t) {
131
- let n = (...r) => {
132
- t.call(this, ...r), this.removeEventListener(e, n);
133
- };
134
- return this.addEventListener(e, n);
135
- }
136
- cleanup() {
137
- return this.stop(), Object.keys(this._listeners).forEach((e) => this.removeEventListener(e)), this._instance?.removeEventListener(a.eventTypes.END, this._onEnd), this._instance?.removeEventListener(a.eventTypes.START, this._onStart), this._instance?.removeEventListener(a.eventTypes.ERROR, this._onError), this._instance?.removeEventListener(a.eventTypes.RESULT, this._onResult), this._instance = null, this;
138
- }
139
- _restart = () => {
140
- this._restartTimeoutId = null;
34
+ if (f.lang = b.lang, f.continuous = b.continuous, f.interimResults = b.interimResults, f.maxAlternatives = b.maxAlternatives, b.grammars) f.grammars = b.grammars;
35
+ else {
36
+ let e = c();
37
+ f.grammars = e ? new e() : null;
38
+ }
39
+ let x = () => {
40
+ _ !== null && (clearTimeout(_), _ = null), v = !1;
41
+ }, S = () => !!f && !h && f.continuous, C = () => {
42
+ _ = null;
141
43
  try {
142
- this._instance.start(), this._lastStartedAt = Date.now();
44
+ f.start(), g = Date.now();
143
45
  } catch {
144
- this._isRestarting = !1, this._isRecording = !1;
46
+ v = !1, m = !1;
145
47
  }
146
- };
147
- _emitAggregatedResult() {
148
- let e = this._finalTranscripts;
149
- if (this._finalTranscripts = [], e.length === 0) return;
48
+ }, w = () => {
49
+ let e = y;
50
+ if (y = [], e.length === 0 || !p[r.RESULT]?.length) return;
150
51
  let t = e.join(" ").trim(), n = Object.assign([{
151
52
  transcript: t,
152
53
  confidence: 1
153
- }], { isFinal: !0 }), r = Object.assign(new Event(a.eventTypes.RESULT), {
54
+ }], { isFinal: !0 }), i = Object.assign(new Event(r.RESULT), {
154
55
  resultIndex: 0,
155
56
  results: [n]
156
57
  });
157
- [...this._listeners[a.eventTypes.RESULT] ?? []].forEach(({ handler: e }) => e(r));
158
- }
159
- static _pickBestAlternative(e) {
160
- return e.reduce((e, t) => (t.confidence ?? 0) > (e.confidence ?? 0) ? t : e);
161
- }
162
- _shouldAutoRestart() {
163
- return !!this._instance && !this._explicitStop && this._instance.continuous;
164
- }
165
- _clearRestartTimeout() {
166
- this._restartTimeoutId !== null && (clearTimeout(this._restartTimeoutId), this._restartTimeoutId = null), this._isRestarting = !1;
167
- }
168
- _includesEventType(e) {
169
- return Object.values(a.eventTypes).includes(e);
170
- }
171
- _unknownEventTypeMessage(e) {
172
- return `Unknown event type "${e}". Valid types are: ${Object.values(a.eventTypes).join(", ")}.`;
173
- }
174
- static _resolveSpeechRecognition() {
175
- if (!(typeof window > "u")) return window.SpeechRecognition ?? window.webkitSpeechRecognition ?? window.mozSpeechRecognition ?? window.msSpeechRecognition;
176
- }
177
- static _resolveSpeechGrammarList() {
178
- return window.SpeechGrammarList ?? window.webkitSpeechGrammarList ?? window.mozSpeechGrammarList ?? window.msSpeechGrammarList;
179
- }
58
+ [...p[r.RESULT]].forEach(({ handler: e }) => e(i));
59
+ }, T = [
60
+ [r.END, (e) => {
61
+ if (S()) {
62
+ let t = Math.max(0, i - (Date.now() - g));
63
+ v = !0, _ = setTimeout(C, t), e.stopImmediatePropagation();
64
+ return;
65
+ }
66
+ m = !1;
67
+ }],
68
+ [r.START, (e) => {
69
+ v && (e.stopImmediatePropagation(), queueMicrotask(() => {
70
+ v = !1;
71
+ }));
72
+ }],
73
+ [r.ERROR, (e) => {
74
+ a.has(e.error) && (h = !0, x(), m = !1);
75
+ }],
76
+ [r.RESULT, (e) => {
77
+ let t = e, n = t.results?.[t.resultIndex];
78
+ n?.isFinal && y.push(l(Array.from(n)).transcript);
79
+ }]
80
+ ];
81
+ T.forEach(([e, t]) => f.addEventListener(e, t));
82
+ let E = async ({ signal: t } = {}) => {
83
+ if (f) try {
84
+ let n = await e("microphone", { audio: !0 }, { signal: t });
85
+ if (t?.aborted) return;
86
+ if (!n) throw Error("Unable to retrieve the stream from media device");
87
+ h = !1, y = [], f.start(), m = !0, g = Date.now();
88
+ } catch (e) {
89
+ if (e instanceof Error && e.name === "AbortError") return;
90
+ throw e;
91
+ }
92
+ }, D = () => {
93
+ f && (h = !0, x(), w(), f.stop(), m = !1);
94
+ }, O = () => {
95
+ f && (h = !0, x(), f.abort(), m = !1, y = []);
96
+ }, k = (e, t) => {
97
+ if (!u(e)) throw Error(d(e));
98
+ if (!f) return;
99
+ let n = (n) => {
100
+ if (v && (e === r.END || e === r.START)) return;
101
+ if (e !== r.RESULT) {
102
+ t(n);
103
+ return;
104
+ }
105
+ let i = n;
106
+ if (!(i.results?.length > 0) || i.resultIndex >= i.results.length) {
107
+ t(n);
108
+ return;
109
+ }
110
+ let a = Array.from(i.results[i.resultIndex]);
111
+ t(n, l(a).transcript, a.map((e) => e.transcript));
112
+ };
113
+ f.addEventListener(e, n), p[e] || (p[e] = []), p[e].push({
114
+ callback: t,
115
+ handler: n
116
+ });
117
+ }, A = (e, t) => {
118
+ if (!u(e)) throw Error(d(e));
119
+ if (!(!f || !p[e])) if (t !== void 0) {
120
+ let n = p[e].findIndex((e) => e.callback === t);
121
+ n !== -1 && (f.removeEventListener(e, p[e][n].handler), p[e].splice(n, 1), p[e].length === 0 && delete p[e]);
122
+ } else p[e].forEach(({ handler: t }) => f.removeEventListener(e, t)), delete p[e];
123
+ };
124
+ return {
125
+ get isRecording() {
126
+ return m;
127
+ },
128
+ start: E,
129
+ stop: D,
130
+ abort: O,
131
+ on: k,
132
+ off: A,
133
+ cleanup: () => {
134
+ D(), Object.keys(p).forEach((e) => A(e)), T.forEach(([e, t]) => f?.removeEventListener(e, t)), f = null;
135
+ }
136
+ };
180
137
  };
181
138
  //#endregion
182
- export { a as Vocal };
139
+ export { p as createVocal, r as eventTypes, f as isSupported };
183
140
 
184
141
  //# sourceMappingURL=index.es.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@untemps/vocal",
3
- "version": "2.0.0-beta.20",
3
+ "version": "2.0.0-beta.22",
4
4
  "description": "Class wrapped around the SpeechRecognition Web API",
5
5
  "repository": "git@github.com:untemps/vocal.git",
6
6
  "keywords": [