@untemps/vocal 2.0.0-beta.19 → 2.0.0-beta.20
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 +17 -0
- package/README.md +13 -1
- package/dist/index.cjs +1 -1
- package/dist/index.es.js +79 -23
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
# [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
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* Auto-restart recognition on silence in continuous mode ([#84](https://github.com/untemps/vocal/issues/84)) ([79a55f5](https://github.com/untemps/vocal/commit/79a55f5e295d2027a1473ce59872e6a09b4655c1))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### BREAKING CHANGES
|
|
10
|
+
|
|
11
|
+
* continuous mode now keeps the session alive across silence and aggregates results — semantics that callers using `continuous: true` must adapt to:
|
|
12
|
+
- Recording no longer ends after ~7s of silence; call `stop()` or `abort()` explicitly to terminate the session.
|
|
13
|
+
- A synthetic `result` event is emitted just before `end` on `stop()`, carrying the joined final transcripts. `event instanceof SpeechRecognitionEvent` returns `false` for this event — read the transcript through the listener's second argument (`(event, bestAlternative, alternatives) => ...`).
|
|
14
|
+
- Intermediate `end` and `start` events fired by the browser during silent restart cycles are no longer forwarded to user listeners. `isRecording` stays `true` across the cycle.
|
|
15
|
+
- `abort()` discards the aggregated buffer without emitting.
|
|
16
|
+
`continuous: false` consumers see no behavioural change.
|
|
17
|
+
|
|
1
18
|
# [2.0.0-beta.19](https://github.com/untemps/vocal/compare/v2.0.0-beta.18...v2.0.0-beta.19) (2026-05-17)
|
|
2
19
|
|
|
3
20
|
# [2.0.0-beta.18](https://github.com/untemps/vocal/compare/v2.0.0-beta.17...v2.0.0-beta.18) (2026-05-16)
|
package/README.md
CHANGED
|
@@ -62,10 +62,22 @@ Please refer to [this section](https://developer.mozilla.org/en-US/docs/Web/API/
|
|
|
62
62
|
| ---------------- | ----------------- | ---------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
63
63
|
| grammars | SpeechGrammarList | null | Grammars understood by the recognition [JSpeech Grammar Format](https://www.w3.org/TR/jsgf/) |
|
|
64
64
|
| lang | string | 'en-US' | Language understood by the recognition [BCP 47 language tag](https://tools.ietf.org/html/bcp47) |
|
|
65
|
-
| continuous | boolean | false | Whether continuous results are returned for each recognition, or only a single result
|
|
65
|
+
| continuous | boolean | false | Whether continuous results are returned for each recognition, or only a single result (see [Continuous mode](#continuous-mode)) |
|
|
66
66
|
| interimResults | boolean | false | Whether interim results should be returned or not. Interim results are results that are not yet final |
|
|
67
67
|
| maxAlternatives | number | 1 | Maximum number of SpeechRecognitionAlternatives provided per result |
|
|
68
68
|
|
|
69
|
+
### Continuous mode
|
|
70
|
+
|
|
71
|
+
Browsers (notably Chrome) automatically end a recognition session after a few seconds of silence, even when `continuous` is `true`. Vocal transparently restarts the underlying engine after such a silence-induced `end`, so recording keeps running until `stop()` or `abort()` is explicitly called. The intermediate `end` and `start` events triggered by the restart are not forwarded to user listeners — `isRecording` stays `true` across the restart, and the cycle is throttled to at most one restart per second to avoid `InvalidStateError`.
|
|
72
|
+
|
|
73
|
+
The restart is disabled automatically when the recognition emits a fatal error (`not-allowed`, `service-not-allowed`, `audio-capture`).
|
|
74
|
+
|
|
75
|
+
#### Aggregated result on stop
|
|
76
|
+
|
|
77
|
+
To compensate for results being split across silent restart cycles, Vocal accumulates every final result (`isFinal: true`) received during a session. On explicit `stop()`, an extra `result` event is emitted just before `end`, carrying the joined transcripts as a single string. Interim results and `abort()` are excluded — `abort()` discards the buffer without emitting.
|
|
78
|
+
|
|
79
|
+
The aggregated event is a synthetic `Event` shaped to match `SpeechRecognitionEvent` (`resultIndex` + `results[0][0].transcript`); it is not a real `SpeechRecognitionEvent` instance, so `event instanceof SpeechRecognitionEvent` returns `false`. Read the transcript through the second argument of the listener (`bestAlternative`).
|
|
80
|
+
|
|
69
81
|
## Events
|
|
70
82
|
|
|
71
83
|
Events described below are those from the `SpeechRecognition` Web API.
|
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=class
|
|
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;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.es.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
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 =
|
|
3
|
+
var r = 1e3, i = new Set([
|
|
4
|
+
"not-allowed",
|
|
5
|
+
"service-not-allowed",
|
|
6
|
+
"audio-capture"
|
|
7
|
+
]), a = class a {
|
|
4
8
|
static defaultOptions = {
|
|
5
9
|
grammars: null,
|
|
6
10
|
lang: "en-US",
|
|
@@ -22,7 +26,7 @@ var r = class r {
|
|
|
22
26
|
START: "start"
|
|
23
27
|
};
|
|
24
28
|
static get isSupported() {
|
|
25
|
-
return !!
|
|
29
|
+
return !!a._resolveSpeechRecognition() && !!n() && !!t();
|
|
26
30
|
}
|
|
27
31
|
static set isSupported(e) {
|
|
28
32
|
throw Error("You cannot set isSupported directly.");
|
|
@@ -30,23 +34,45 @@ var r = class r {
|
|
|
30
34
|
_instance = null;
|
|
31
35
|
_listeners = {};
|
|
32
36
|
_isRecording = !1;
|
|
33
|
-
|
|
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
|
+
}
|
|
34
48
|
this._isRecording = !1;
|
|
35
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
|
+
};
|
|
36
62
|
constructor(e) {
|
|
37
|
-
let t =
|
|
63
|
+
let t = a._resolveSpeechRecognition();
|
|
38
64
|
if (!t) throw new DOMException("SpeechRecognition not supported", "NOT_SUPPORTED_ERR");
|
|
39
65
|
this._instance = new t();
|
|
40
|
-
let { grammars: n, ...
|
|
41
|
-
...
|
|
66
|
+
let { grammars: n, ...r } = {
|
|
67
|
+
...a.defaultOptions,
|
|
42
68
|
...e ?? {}
|
|
43
|
-
},
|
|
44
|
-
if (Object.assign(
|
|
69
|
+
}, i = this._instance;
|
|
70
|
+
if (Object.assign(i, r), n) i.grammars = n;
|
|
45
71
|
else {
|
|
46
|
-
let e =
|
|
47
|
-
|
|
72
|
+
let e = a._resolveSpeechGrammarList();
|
|
73
|
+
i.grammars = e ? new e() : null;
|
|
48
74
|
}
|
|
49
|
-
this._instance.addEventListener(
|
|
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);
|
|
50
76
|
}
|
|
51
77
|
get isRecording() {
|
|
52
78
|
return this._isRecording;
|
|
@@ -57,7 +83,7 @@ var r = class r {
|
|
|
57
83
|
async start({ signal: t } = {}) {
|
|
58
84
|
if (this._instance) try {
|
|
59
85
|
if (!await e("microphone", { audio: !0 }, { signal: t })) throw Error("Unable to retrieve the stream from media device");
|
|
60
|
-
this._instance.start(), this._isRecording = !0;
|
|
86
|
+
this._explicitStop = !1, this._finalTranscripts = [], this._instance.start(), this._isRecording = !0, this._lastStartedAt = Date.now();
|
|
61
87
|
} catch (e) {
|
|
62
88
|
if (e instanceof Error && e.name === "AbortError") return this;
|
|
63
89
|
throw e;
|
|
@@ -65,24 +91,25 @@ var r = class r {
|
|
|
65
91
|
return this;
|
|
66
92
|
}
|
|
67
93
|
stop() {
|
|
68
|
-
return this._instance && (this._instance.stop(), this._isRecording = !1), this;
|
|
94
|
+
return this._instance && (this._explicitStop = !0, this._clearRestartTimeout(), this._emitAggregatedResult(), this._instance.stop(), this._isRecording = !1), this;
|
|
69
95
|
}
|
|
70
96
|
abort() {
|
|
71
|
-
return this._instance && (this._instance.abort(), this._isRecording = !1), this;
|
|
97
|
+
return this._instance && (this._explicitStop = !0, this._clearRestartTimeout(), this._instance.abort(), this._isRecording = !1, this._finalTranscripts = []), this;
|
|
72
98
|
}
|
|
73
99
|
addEventListener(e, t) {
|
|
74
100
|
if (!this._includesEventType(e)) throw Error(this._unknownEventTypeMessage(e));
|
|
75
101
|
if (this._instance) {
|
|
76
102
|
let n = (n) => {
|
|
77
|
-
|
|
78
|
-
|
|
103
|
+
if (this._isRestarting && (e === a.eventTypes.END || e === a.eventTypes.START)) return;
|
|
104
|
+
let r = [];
|
|
105
|
+
if (e === a.eventTypes.RESULT) {
|
|
79
106
|
let e = n;
|
|
80
107
|
if (e.results?.length > 0 && e.resultIndex < e.results.length) {
|
|
81
|
-
let t = Array.from(e.results[e.resultIndex])
|
|
82
|
-
|
|
108
|
+
let t = Array.from(e.results[e.resultIndex]);
|
|
109
|
+
r.push(a._pickBestAlternative(t).transcript, t.map((e) => e.transcript));
|
|
83
110
|
}
|
|
84
111
|
}
|
|
85
|
-
t.call(this, n, ...
|
|
112
|
+
t.call(this, n, ...r);
|
|
86
113
|
};
|
|
87
114
|
this._instance.addEventListener(e, n), this._listeners[e] || (this._listeners[e] = []), this._listeners[e].push({
|
|
88
115
|
callback: t,
|
|
@@ -107,13 +134,42 @@ var r = class r {
|
|
|
107
134
|
return this.addEventListener(e, n);
|
|
108
135
|
}
|
|
109
136
|
cleanup() {
|
|
110
|
-
return this.stop(), Object.keys(this._listeners).forEach((e) => this.removeEventListener(e)), this._instance?.removeEventListener(
|
|
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;
|
|
141
|
+
try {
|
|
142
|
+
this._instance.start(), this._lastStartedAt = Date.now();
|
|
143
|
+
} catch {
|
|
144
|
+
this._isRestarting = !1, this._isRecording = !1;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
_emitAggregatedResult() {
|
|
148
|
+
let e = this._finalTranscripts;
|
|
149
|
+
if (this._finalTranscripts = [], e.length === 0) return;
|
|
150
|
+
let t = e.join(" ").trim(), n = Object.assign([{
|
|
151
|
+
transcript: t,
|
|
152
|
+
confidence: 1
|
|
153
|
+
}], { isFinal: !0 }), r = Object.assign(new Event(a.eventTypes.RESULT), {
|
|
154
|
+
resultIndex: 0,
|
|
155
|
+
results: [n]
|
|
156
|
+
});
|
|
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;
|
|
111
167
|
}
|
|
112
168
|
_includesEventType(e) {
|
|
113
|
-
return Object.values(
|
|
169
|
+
return Object.values(a.eventTypes).includes(e);
|
|
114
170
|
}
|
|
115
171
|
_unknownEventTypeMessage(e) {
|
|
116
|
-
return `Unknown event type "${e}". Valid types are: ${Object.values(
|
|
172
|
+
return `Unknown event type "${e}". Valid types are: ${Object.values(a.eventTypes).join(", ")}.`;
|
|
117
173
|
}
|
|
118
174
|
static _resolveSpeechRecognition() {
|
|
119
175
|
if (!(typeof window > "u")) return window.SpeechRecognition ?? window.webkitSpeechRecognition ?? window.mozSpeechRecognition ?? window.msSpeechRecognition;
|
|
@@ -123,6 +179,6 @@ var r = class r {
|
|
|
123
179
|
}
|
|
124
180
|
};
|
|
125
181
|
//#endregion
|
|
126
|
-
export {
|
|
182
|
+
export { a as Vocal };
|
|
127
183
|
|
|
128
184
|
//# sourceMappingURL=index.es.js.map
|