@untemps/vocal 2.0.0-beta.21 → 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,35 @@
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
+
1
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)
2
34
 
3
35
 
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'`). |
101
109
 
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 |
110
+ ## Instance getter
111
+
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
 
@@ -152,5 +163,24 @@ Throws if `eventType` is not a valid `EventType`.
152
163
 
153
164
  ### `cleanup()`
154
165
 
155
- Stops recognition, removes all registered listeners, and releases the internal `SpeechRecognition` instance. The `Vocal` object cannot be reused after `cleanup()`.
166
+ Stops recognition, removes all registered listeners, and releases the internal `SpeechRecognition` instance. The returned `VocalInstance` cannot be reused after `cleanup()`.
167
+
168
+ ## Migration from the class-based API (v1.x)
169
+
170
+ ```js
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)
184
+ ```
156
185
 
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}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,178 +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
- cleanup() {
131
- 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;
132
- }
133
- _restart = () => {
134
- 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;
135
43
  try {
136
- this._instance.start(), this._lastStartedAt = Date.now();
44
+ f.start(), g = Date.now();
137
45
  } catch {
138
- this._isRestarting = !1, this._isRecording = !1;
46
+ v = !1, m = !1;
139
47
  }
140
- };
141
- _emitAggregatedResult() {
142
- let e = this._finalTranscripts;
143
- if (this._finalTranscripts = [], e.length === 0) return;
48
+ }, w = () => {
49
+ let e = y;
50
+ if (y = [], e.length === 0 || !p[r.RESULT]?.length) return;
144
51
  let t = e.join(" ").trim(), n = Object.assign([{
145
52
  transcript: t,
146
53
  confidence: 1
147
- }], { isFinal: !0 }), r = Object.assign(new Event(a.eventTypes.RESULT), {
54
+ }], { isFinal: !0 }), i = Object.assign(new Event(r.RESULT), {
148
55
  resultIndex: 0,
149
56
  results: [n]
150
57
  });
151
- [...this._listeners[a.eventTypes.RESULT] ?? []].forEach(({ handler: e }) => e(r));
152
- }
153
- static _pickBestAlternative(e) {
154
- return e.reduce((e, t) => (t.confidence ?? 0) > (e.confidence ?? 0) ? t : e);
155
- }
156
- _shouldAutoRestart() {
157
- return !!this._instance && !this._explicitStop && this._instance.continuous;
158
- }
159
- _clearRestartTimeout() {
160
- this._restartTimeoutId !== null && (clearTimeout(this._restartTimeoutId), this._restartTimeoutId = null), this._isRestarting = !1;
161
- }
162
- _includesEventType(e) {
163
- return Object.values(a.eventTypes).includes(e);
164
- }
165
- _unknownEventTypeMessage(e) {
166
- return `Unknown event type "${e}". Valid types are: ${Object.values(a.eventTypes).join(", ")}.`;
167
- }
168
- static _resolveSpeechRecognition() {
169
- if (!(typeof window > "u")) return window.SpeechRecognition ?? window.webkitSpeechRecognition ?? window.mozSpeechRecognition ?? window.msSpeechRecognition;
170
- }
171
- static _resolveSpeechGrammarList() {
172
- return window.SpeechGrammarList ?? window.webkitSpeechGrammarList ?? window.mozSpeechGrammarList ?? window.msSpeechGrammarList;
173
- }
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
+ };
174
137
  };
175
138
  //#endregion
176
- export { a as Vocal };
139
+ export { p as createVocal, r as eventTypes, f as isSupported };
177
140
 
178
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.21",
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": [