dsh-live-voice 0.2.0 → 0.2.2

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.
@@ -39,12 +39,18 @@ export function encodeMonoPcm16Wav(samples, inputRate) {
39
39
  return buffer;
40
40
  }
41
41
  export class WhisperHttpRecognitionEngine {
42
- constructor({ globals = globalThis, meter, voiceDetectionPreset = 'natural' } = {}) {
42
+ constructor({
43
+ globals = globalThis,
44
+ meter,
45
+ voiceDetectionPreset = 'natural',
46
+ maxUtteranceSeconds = 60,
47
+ } = {}) {
43
48
  this.g = globals;
44
49
  this.meter = meter;
45
50
  this.session = null;
46
51
  this.lang = 'pt-BR';
47
52
  this.voiceDetectionPreset = voiceDetectionPreset;
53
+ this.maxUtteranceSeconds = maxUtteranceSeconds;
48
54
  this.route = ROUTE;
49
55
  }
50
56
  get segmentation() {
@@ -73,7 +79,14 @@ export class WhisperHttpRecognitionEngine {
73
79
  };
74
80
  }
75
81
  }
76
- async start({ lang = this.lang, signal, onResult, onActivity, onError } = {}) {
82
+ async start({
83
+ lang = this.lang,
84
+ signal,
85
+ onResult,
86
+ onActivity,
87
+ onError,
88
+ onProcessingChange,
89
+ } = {}) {
77
90
  await this.stop();
78
91
  if (signal?.aborted) throw abortError();
79
92
  const context = this.meter?.context,
@@ -95,16 +108,25 @@ export class WhisperHttpRecognitionEngine {
95
108
  samples: 0,
96
109
  voiced: false,
97
110
  silence: 0,
98
- inflight: new Set(),
111
+ transcriptionQueue: [],
112
+ activeRequest: null,
113
+ draining: false,
99
114
  onResult,
100
115
  onActivity,
101
116
  onError,
117
+ onProcessingChange,
102
118
  lang,
103
119
  signal,
104
120
  };
105
121
  this.session = session;
106
122
  const valid = () => this.session === session && !signal?.aborted;
107
- const submit = async () => {
123
+ const notifyProcessing = () =>
124
+ session.onProcessingChange?.({
125
+ queued: session.transcriptionQueue.length,
126
+ active: !!session.activeRequest,
127
+ pending: session.transcriptionQueue.length + (session.activeRequest ? 1 : 0),
128
+ });
129
+ const enqueue = () => {
108
130
  if (!session.voiced || session.samples < context.sampleRate * 0.25) {
109
131
  session.chunks = [];
110
132
  session.samples = 0;
@@ -122,29 +144,46 @@ export class WhisperHttpRecognitionEngine {
122
144
  session.samples = 0;
123
145
  session.voiced = false;
124
146
  session.silence = 0;
125
- const request = new AbortController();
126
- session.inflight.add(request);
147
+ session.transcriptionQueue.push(samples);
148
+ notifyProcessing();
149
+ void drain();
150
+ };
151
+ const drain = async () => {
152
+ if (session.draining || !valid()) return;
153
+ session.draining = true;
127
154
  try {
128
- const response = await this.g.fetch(this.route + '/transcribe', {
129
- method: 'POST',
130
- credentials: 'same-origin',
131
- headers: {
132
- 'content-type': 'audio/wav',
133
- 'x-dlv-client-id': session.operation,
134
- 'x-dlv-operation-id': id(),
135
- 'x-dlv-language': lang,
136
- },
137
- body: encodeMonoPcm16Wav(samples, context.sampleRate),
138
- signal: request.signal,
139
- });
140
- const json = await response.json();
141
- if (!response.ok || !json?.ok)
142
- throw new Error(json?.error?.message || 'HTTP transcription failed.');
143
- if (valid() && json.value.text) onResult?.({ final: json.value.text, interim: '' });
144
- } catch (error) {
145
- if (error.name !== 'AbortError' && valid()) onError?.(error);
155
+ while (valid() && session.transcriptionQueue.length) {
156
+ const samples = session.transcriptionQueue.shift();
157
+ const request = new AbortController();
158
+ session.activeRequest = request;
159
+ notifyProcessing();
160
+ try {
161
+ const response = await this.g.fetch(this.route + '/transcribe', {
162
+ method: 'POST',
163
+ credentials: 'same-origin',
164
+ headers: {
165
+ 'content-type': 'audio/wav',
166
+ 'x-dlv-client-id': session.operation,
167
+ 'x-dlv-operation-id': id(),
168
+ 'x-dlv-language': lang,
169
+ },
170
+ body: encodeMonoPcm16Wav(samples, context.sampleRate),
171
+ signal: request.signal,
172
+ });
173
+ const json = await response.json();
174
+ if (!response.ok || !json?.ok)
175
+ throw new Error(json?.error?.message || 'HTTP transcription failed.');
176
+ if (valid() && json.value.text) onResult?.({ final: json.value.text, interim: '' });
177
+ } catch (error) {
178
+ if (error.name !== 'AbortError' && valid()) onError?.(error);
179
+ } finally {
180
+ if (session.activeRequest === request) session.activeRequest = null;
181
+ notifyProcessing();
182
+ }
183
+ }
146
184
  } finally {
147
- session.inflight.delete(request);
185
+ session.draining = false;
186
+ notifyProcessing();
148
187
  }
149
188
  };
150
189
  processor.onaudioprocess = (event) => {
@@ -164,14 +203,24 @@ export class WhisperHttpRecognitionEngine {
164
203
  if (
165
204
  (session.voiced &&
166
205
  session.silence > context.sampleRate * (this.segmentation.silenceMs / 1000)) ||
167
- session.samples > context.sampleRate * 20
206
+ session.samples > context.sampleRate * this.maxUtteranceSeconds
168
207
  )
169
- void submit();
208
+ enqueue();
170
209
  };
171
210
  source.connect(processor);
211
+ session.finish = enqueue;
172
212
  session.abort = () => this.stop();
173
213
  signal?.addEventListener('abort', session.abort, { once: true });
174
214
  }
215
+ finish() {
216
+ const session = this.session;
217
+ if (!session) return;
218
+ session.processor.onaudioprocess = null;
219
+ try {
220
+ this.meter?.source?.disconnect(session.processor);
221
+ } catch {}
222
+ session.finish?.();
223
+ }
175
224
  async stop() {
176
225
  const session = this.session;
177
226
  if (!session) return;
@@ -185,7 +234,9 @@ export class WhisperHttpRecognitionEngine {
185
234
  session.processor.disconnect();
186
235
  session.gain?.disconnect();
187
236
  } catch {}
188
- for (const request of session.inflight) request.abort();
189
- session.inflight.clear();
237
+ session.transcriptionQueue = [];
238
+ session.activeRequest?.abort();
239
+ session.activeRequest = null;
240
+ session.onProcessingChange?.({ queued: 0, active: false, pending: 0 });
190
241
  }
191
242
  }