@eka-care/ekascribe-ts-sdk 1.4.39
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 +437 -0
- package/dist/api/get-transaction-history.d.ts +5 -0
- package/dist/api/get-transaction-history.js +28 -0
- package/dist/api/get-voice-api-v2-config.d.ts +2 -0
- package/dist/api/get-voice-api-v2-config.js +26 -0
- package/dist/api/get-voice-api-v2-status.d.ts +51 -0
- package/dist/api/get-voice-api-v2-status.js +25 -0
- package/dist/api/get-voice-api-v3-status.d.ts +51 -0
- package/dist/api/get-voice-api-v3-status.js +26 -0
- package/dist/api/patch-transaction-status.d.ts +4 -0
- package/dist/api/patch-transaction-status.js +43 -0
- package/dist/api/post-cog-init.d.ts +3 -0
- package/dist/api/post-cog-init.js +15 -0
- package/dist/api/post-transaction-commit.d.ts +3 -0
- package/dist/api/post-transaction-commit.js +32 -0
- package/dist/api/post-transaction-init.d.ts +3 -0
- package/dist/api/post-transaction-init.js +40 -0
- package/dist/api/post-transaction-stop.d.ts +3 -0
- package/dist/api/post-transaction-stop.js +32 -0
- package/dist/audio-chunker/__tests__/audio-file-manager.test.d.ts +1 -0
- package/dist/audio-chunker/__tests__/audio-file-manager.test.js +5 -0
- package/dist/audio-chunker/audio-buffer-manager.d.ts +53 -0
- package/dist/audio-chunker/audio-buffer-manager.js +136 -0
- package/dist/audio-chunker/audio-file-manager.d.ts +96 -0
- package/dist/audio-chunker/audio-file-manager.js +579 -0
- package/dist/audio-chunker/vad-web.d.ts +90 -0
- package/dist/audio-chunker/vad-web.js +389 -0
- package/dist/aws-services/configure-aws.d.ts +5 -0
- package/dist/aws-services/configure-aws.js +13 -0
- package/dist/aws-services/get-files-s3.d.ts +10 -0
- package/dist/aws-services/get-files-s3.js +30 -0
- package/dist/aws-services/s3-retry-wrapper.d.ts +2 -0
- package/dist/aws-services/s3-retry-wrapper.js +38 -0
- package/dist/aws-services/translate-text-to-target-language.d.ts +6 -0
- package/dist/aws-services/translate-text-to-target-language.js +18 -0
- package/dist/aws-services/upload-file-to-s3.d.ts +13 -0
- package/dist/aws-services/upload-file-to-s3.js +48 -0
- package/dist/constants/constant.d.ts +27 -0
- package/dist/constants/constant.js +33 -0
- package/dist/constants/enums.d.ts +46 -0
- package/dist/constants/enums.js +51 -0
- package/dist/constants/setup-config.d.ts +14 -0
- package/dist/constants/setup-config.js +31 -0
- package/dist/constants/types.d.ts +224 -0
- package/dist/constants/types.js +1 -0
- package/dist/fetch-client/helper.d.ts +11 -0
- package/dist/fetch-client/helper.js +28 -0
- package/dist/fetch-client/index.d.ts +1 -0
- package/dist/fetch-client/index.js +36 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +267 -0
- package/dist/main/end-recording.d.ts +3 -0
- package/dist/main/end-recording.js +141 -0
- package/dist/main/init-transaction.d.ts +3 -0
- package/dist/main/init-transaction.js +86 -0
- package/dist/main/pause-recording.d.ts +3 -0
- package/dist/main/pause-recording.js +59 -0
- package/dist/main/resume-recording.d.ts +3 -0
- package/dist/main/resume-recording.js +33 -0
- package/dist/main/retry-upload-recording.d.ts +5 -0
- package/dist/main/retry-upload-recording.js +69 -0
- package/dist/main/start-recording.d.ts +3 -0
- package/dist/main/start-recording.js +55 -0
- package/dist/shared-worker/s3-file-upload.d.ts +1 -0
- package/dist/shared-worker/s3-file-upload.js +109 -0
- package/dist/shared-worker/s3-file-upload.ts +126 -0
- package/dist/store/store.d.ts +35 -0
- package/dist/store/store.js +121 -0
- package/dist/utils/compress-mp3-audio.d.ts +2 -0
- package/dist/utils/compress-mp3-audio.js +24 -0
- package/package.json +53 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import { AUDIO_EXTENSION_TYPE_MAP, OUTPUT_FORMAT } from '../constants/constant';
|
|
2
|
+
import pushFileToS3 from '../aws-services/upload-file-to-s3';
|
|
3
|
+
import postCogInit from '../api/post-cog-init';
|
|
4
|
+
import { configureAWS } from '../aws-services/configure-aws';
|
|
5
|
+
import { SHARED_WORKER_ACTION } from '../constants/enums';
|
|
6
|
+
import compressAudioToMp3 from '../utils/compress-mp3-audio';
|
|
7
|
+
class AudioFileManager {
|
|
8
|
+
initialiseClassInstance() {
|
|
9
|
+
this.audioChunks = [];
|
|
10
|
+
this.uploadPromises = [];
|
|
11
|
+
this.successfulUploads = [];
|
|
12
|
+
this.totalInsertedFrames = 0;
|
|
13
|
+
this.totalInsertedSamples = 0;
|
|
14
|
+
this.totalRawSamples = 0;
|
|
15
|
+
this.totalRawFrames = 0;
|
|
16
|
+
}
|
|
17
|
+
constructor() {
|
|
18
|
+
/**
|
|
19
|
+
* Class that handles uploading audio files to S3
|
|
20
|
+
* and downloading audio files for debugging
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(this, "txnID", {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
writable: true,
|
|
26
|
+
value: ''
|
|
27
|
+
});
|
|
28
|
+
Object.defineProperty(this, "filePath", {
|
|
29
|
+
enumerable: true,
|
|
30
|
+
configurable: true,
|
|
31
|
+
writable: true,
|
|
32
|
+
value: ''
|
|
33
|
+
});
|
|
34
|
+
Object.defineProperty(this, "audioChunks", {
|
|
35
|
+
enumerable: true,
|
|
36
|
+
configurable: true,
|
|
37
|
+
writable: true,
|
|
38
|
+
value: []
|
|
39
|
+
});
|
|
40
|
+
Object.defineProperty(this, "uploadPromises", {
|
|
41
|
+
enumerable: true,
|
|
42
|
+
configurable: true,
|
|
43
|
+
writable: true,
|
|
44
|
+
value: []
|
|
45
|
+
});
|
|
46
|
+
Object.defineProperty(this, "successfulUploads", {
|
|
47
|
+
enumerable: true,
|
|
48
|
+
configurable: true,
|
|
49
|
+
writable: true,
|
|
50
|
+
value: []
|
|
51
|
+
});
|
|
52
|
+
Object.defineProperty(this, "onProgressCallback", {
|
|
53
|
+
enumerable: true,
|
|
54
|
+
configurable: true,
|
|
55
|
+
writable: true,
|
|
56
|
+
value: void 0
|
|
57
|
+
});
|
|
58
|
+
Object.defineProperty(this, "totalRawSamples", {
|
|
59
|
+
enumerable: true,
|
|
60
|
+
configurable: true,
|
|
61
|
+
writable: true,
|
|
62
|
+
value: 0
|
|
63
|
+
});
|
|
64
|
+
Object.defineProperty(this, "totalRawFrames", {
|
|
65
|
+
enumerable: true,
|
|
66
|
+
configurable: true,
|
|
67
|
+
writable: true,
|
|
68
|
+
value: 0
|
|
69
|
+
});
|
|
70
|
+
Object.defineProperty(this, "totalInsertedSamples", {
|
|
71
|
+
enumerable: true,
|
|
72
|
+
configurable: true,
|
|
73
|
+
writable: true,
|
|
74
|
+
value: 0
|
|
75
|
+
});
|
|
76
|
+
Object.defineProperty(this, "totalInsertedFrames", {
|
|
77
|
+
enumerable: true,
|
|
78
|
+
configurable: true,
|
|
79
|
+
writable: true,
|
|
80
|
+
value: 0
|
|
81
|
+
});
|
|
82
|
+
Object.defineProperty(this, "businessID", {
|
|
83
|
+
enumerable: true,
|
|
84
|
+
configurable: true,
|
|
85
|
+
writable: true,
|
|
86
|
+
value: ''
|
|
87
|
+
});
|
|
88
|
+
Object.defineProperty(this, "isAWSConfigured", {
|
|
89
|
+
enumerable: true,
|
|
90
|
+
configurable: true,
|
|
91
|
+
writable: true,
|
|
92
|
+
value: false
|
|
93
|
+
});
|
|
94
|
+
Object.defineProperty(this, "sharedWorkerInstance", {
|
|
95
|
+
enumerable: true,
|
|
96
|
+
configurable: true,
|
|
97
|
+
writable: true,
|
|
98
|
+
value: null
|
|
99
|
+
});
|
|
100
|
+
this.initialiseClassInstance();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Set basic file information
|
|
104
|
+
*/
|
|
105
|
+
setSessionInfo({ sessionId, filePath, businessID, }) {
|
|
106
|
+
this.txnID = sessionId;
|
|
107
|
+
this.filePath = filePath;
|
|
108
|
+
this.businessID = businessID;
|
|
109
|
+
}
|
|
110
|
+
getRawSampleDetails() {
|
|
111
|
+
return {
|
|
112
|
+
totalRawSamples: this.totalRawSamples,
|
|
113
|
+
totalRawFrames: this.totalRawFrames,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
incrementTotalRawSamples(frames) {
|
|
117
|
+
this.totalRawSamples += frames.length;
|
|
118
|
+
this.totalRawFrames += 1;
|
|
119
|
+
}
|
|
120
|
+
incrementInsertedSamples(samples, frames) {
|
|
121
|
+
this.totalInsertedSamples += samples;
|
|
122
|
+
this.totalInsertedFrames += frames;
|
|
123
|
+
}
|
|
124
|
+
getInsertedSampleDetails() {
|
|
125
|
+
return {
|
|
126
|
+
totalInsertedSamples: this.totalInsertedSamples,
|
|
127
|
+
totalInsertedFrames: this.totalInsertedFrames,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Set callback for upload progress updates
|
|
132
|
+
*/
|
|
133
|
+
setProgressCallback(callback) {
|
|
134
|
+
this.onProgressCallback = callback;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Update audio information array, this will update the audio chunks info
|
|
138
|
+
* (+ the latest chunk , affects the length of chunks data struct)
|
|
139
|
+
*/
|
|
140
|
+
updateAudioInfo(audioChunks) {
|
|
141
|
+
this.audioChunks.push(audioChunks);
|
|
142
|
+
return this.audioChunks.length;
|
|
143
|
+
}
|
|
144
|
+
createSharedWorkerInstance() {
|
|
145
|
+
try {
|
|
146
|
+
// new URL(relativeOrAbsolutePath, baseUrl)
|
|
147
|
+
const worker = new SharedWorker(new URL('../shared-worker/s3-file-upload.js', import.meta.url));
|
|
148
|
+
this.sharedWorkerInstance = worker;
|
|
149
|
+
this.sharedWorkerInstance.port.onmessage = async (event) => {
|
|
150
|
+
const workerResponse = event.data;
|
|
151
|
+
switch (workerResponse.action) {
|
|
152
|
+
case SHARED_WORKER_ACTION.CONFIGURE_AWS_SUCCESS: {
|
|
153
|
+
console.log('AWS configured successfully in worker');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
case SHARED_WORKER_ACTION.CONFIGURE_AWS_ERROR: {
|
|
157
|
+
console.error('Error configuring AWS in worker:', workerResponse.error);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
case SHARED_WORKER_ACTION.UPLOAD_FILE_WITH_WORKER_SUCCESS: {
|
|
161
|
+
console.log('File uploaded successfully in worker:', workerResponse.response);
|
|
162
|
+
const { fileCount: fileName, chunkIndex, fileBlob, compressedAudioBuffer, } = workerResponse.requestBody;
|
|
163
|
+
if (this.onProgressCallback && compressedAudioBuffer) {
|
|
164
|
+
this.onProgressCallback({
|
|
165
|
+
success: this.successfulUploads.length,
|
|
166
|
+
total: this.audioChunks.length,
|
|
167
|
+
fileName,
|
|
168
|
+
chunkData: compressedAudioBuffer,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (workerResponse.response.success) {
|
|
172
|
+
this.successfulUploads.push(fileName);
|
|
173
|
+
// remove audioFrames if file uploaded successfully
|
|
174
|
+
if (chunkIndex !== -1) {
|
|
175
|
+
this.audioChunks[chunkIndex] = {
|
|
176
|
+
...this.audioChunks[chunkIndex],
|
|
177
|
+
audioFrames: undefined,
|
|
178
|
+
fileBlob: undefined,
|
|
179
|
+
status: 'success',
|
|
180
|
+
response: workerResponse.response.success,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (this.onProgressCallback) {
|
|
184
|
+
this.onProgressCallback({
|
|
185
|
+
success: this.successfulUploads.length,
|
|
186
|
+
total: this.audioChunks.length,
|
|
187
|
+
is_uploaded: true,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
if (this.onProgressCallback) {
|
|
193
|
+
this.onProgressCallback({
|
|
194
|
+
success: this.successfulUploads.length,
|
|
195
|
+
total: this.audioChunks.length,
|
|
196
|
+
fileName,
|
|
197
|
+
is_uploaded: false,
|
|
198
|
+
error: {
|
|
199
|
+
code: workerResponse.response.code,
|
|
200
|
+
msg: 'Tokens expired.',
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// store that audioFrames in audioChunks
|
|
205
|
+
if (chunkIndex !== -1) {
|
|
206
|
+
this.audioChunks[chunkIndex] = {
|
|
207
|
+
...this.audioChunks[chunkIndex],
|
|
208
|
+
fileBlob,
|
|
209
|
+
audioFrames: undefined,
|
|
210
|
+
status: 'failure',
|
|
211
|
+
response: workerResponse.response.error || 'Upload failed',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// call COG if S3 throws ExpiredToken error
|
|
215
|
+
if (workerResponse.response.errorCode === 'ExpiredToken') {
|
|
216
|
+
this.setupAWSConfiguration({
|
|
217
|
+
is_shared_worker: true,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
worker.port.start();
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
console.error('Error creating shared worker instance:', error);
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async setupAWSConfiguration({ is_shared_worker }) {
|
|
233
|
+
try {
|
|
234
|
+
const response = await postCogInit();
|
|
235
|
+
const { credentials, is_session_expired } = response;
|
|
236
|
+
if (is_session_expired || !credentials) {
|
|
237
|
+
this.isAWSConfigured = false;
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
const { AccessKeyId, SecretKey, SessionToken } = credentials;
|
|
241
|
+
if (is_shared_worker) {
|
|
242
|
+
this.sharedWorkerInstance?.port.postMessage({
|
|
243
|
+
action: SHARED_WORKER_ACTION.CONFIGURE_AWS,
|
|
244
|
+
payload: {
|
|
245
|
+
accessKeyId: AccessKeyId,
|
|
246
|
+
secretKey: SecretKey,
|
|
247
|
+
sessionToken: SessionToken,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
configureAWS({
|
|
253
|
+
accessKeyId: AccessKeyId,
|
|
254
|
+
secretKey: SecretKey,
|
|
255
|
+
sessionToken: SessionToken,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
this.isAWSConfigured = true;
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
console.log('%c Line:198 🥃 error', 'color:#42b983', error);
|
|
263
|
+
this.isAWSConfigured = false;
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Upload a chunk of audio data to S3 in main thread
|
|
269
|
+
*/
|
|
270
|
+
async uploadAudioChunkInMain({ audioFrames, fileName, chunkIndex, }) {
|
|
271
|
+
const s3FileName = `${this.filePath}/${fileName}`; // fileName is ${fileCount}.mp3
|
|
272
|
+
const compressedAudioBuffer = compressAudioToMp3(audioFrames);
|
|
273
|
+
const audioBlob = new Blob(compressedAudioBuffer, {
|
|
274
|
+
type: AUDIO_EXTENSION_TYPE_MAP[OUTPUT_FORMAT],
|
|
275
|
+
});
|
|
276
|
+
if (this.onProgressCallback) {
|
|
277
|
+
this.onProgressCallback({
|
|
278
|
+
success: this.successfulUploads.length,
|
|
279
|
+
total: this.audioChunks.length,
|
|
280
|
+
fileName,
|
|
281
|
+
chunkData: compressedAudioBuffer,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
// Push upload promise to track status
|
|
285
|
+
const uploadPromise = pushFileToS3({
|
|
286
|
+
fileBlob: audioBlob,
|
|
287
|
+
fileName: s3FileName,
|
|
288
|
+
txnID: this.txnID,
|
|
289
|
+
businessID: this.businessID,
|
|
290
|
+
is_shared_worker: false,
|
|
291
|
+
}).then((response) => {
|
|
292
|
+
if (response.success) {
|
|
293
|
+
this.successfulUploads.push(fileName);
|
|
294
|
+
// update file status if file uploaded successfully
|
|
295
|
+
if (chunkIndex !== -1) {
|
|
296
|
+
this.audioChunks[chunkIndex] = {
|
|
297
|
+
...this.audioChunks[chunkIndex],
|
|
298
|
+
audioFrames: undefined,
|
|
299
|
+
fileBlob: undefined,
|
|
300
|
+
status: 'success',
|
|
301
|
+
response: response.success,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
if (this.onProgressCallback) {
|
|
305
|
+
this.onProgressCallback({
|
|
306
|
+
success: this.successfulUploads.length,
|
|
307
|
+
total: this.audioChunks.length,
|
|
308
|
+
is_uploaded: true,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
if (chunkIndex !== -1) {
|
|
314
|
+
this.audioChunks[chunkIndex] = {
|
|
315
|
+
...this.audioChunks[chunkIndex],
|
|
316
|
+
fileBlob: audioBlob,
|
|
317
|
+
audioFrames: undefined,
|
|
318
|
+
status: 'failure',
|
|
319
|
+
response: response.error || 'Upload failed',
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (this.onProgressCallback) {
|
|
323
|
+
this.onProgressCallback({
|
|
324
|
+
success: this.successfulUploads.length,
|
|
325
|
+
total: this.audioChunks.length,
|
|
326
|
+
fileName,
|
|
327
|
+
is_uploaded: false,
|
|
328
|
+
error: {
|
|
329
|
+
code: response.code || 500,
|
|
330
|
+
msg: 'Tokens expired.',
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return response;
|
|
336
|
+
});
|
|
337
|
+
this.uploadPromises.push(uploadPromise);
|
|
338
|
+
return {
|
|
339
|
+
success: true,
|
|
340
|
+
fileName,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Upload audio chunks to S3 in shared worker
|
|
345
|
+
*/
|
|
346
|
+
async uploadAudioChunkInWorker({ audioFrames, fileName, chunkIndex, }) {
|
|
347
|
+
const s3FileName = `${this.filePath}/${fileName}`;
|
|
348
|
+
if (this.onProgressCallback) {
|
|
349
|
+
this.onProgressCallback({
|
|
350
|
+
success: this.successfulUploads.length,
|
|
351
|
+
total: this.audioChunks.length,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
this.sharedWorkerInstance?.port.postMessage({
|
|
355
|
+
action: SHARED_WORKER_ACTION.UPLOAD_FILE_WITH_WORKER,
|
|
356
|
+
payload: {
|
|
357
|
+
audioFrames,
|
|
358
|
+
fileName: s3FileName,
|
|
359
|
+
txnID: this.txnID,
|
|
360
|
+
businessID: this.businessID,
|
|
361
|
+
chunkIndex,
|
|
362
|
+
fileCount: fileName,
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
return {
|
|
366
|
+
success: true,
|
|
367
|
+
fileName,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
async uploadAudioToS3({ audioFrames, fileName, chunkIndex }) {
|
|
371
|
+
if (typeof SharedWorker === 'undefined' || !SharedWorker) {
|
|
372
|
+
// Shared Workers are not supported in this browser
|
|
373
|
+
console.log('Shared Workers are NOT supported in this browser.');
|
|
374
|
+
await this.uploadAudioToS3WithoutWorker({ audioFrames, fileName, chunkIndex });
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
// Shared Workers are supported
|
|
378
|
+
console.log('Shared Workers are supported in this browser.');
|
|
379
|
+
if (!this.sharedWorkerInstance) {
|
|
380
|
+
this.createSharedWorkerInstance();
|
|
381
|
+
}
|
|
382
|
+
await this.uploadAudioToS3WithWorker({ audioFrames, fileName, chunkIndex });
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async uploadAudioToS3WithWorker({ audioFrames, fileName, chunkIndex, }) {
|
|
386
|
+
try {
|
|
387
|
+
if (!this.isAWSConfigured) {
|
|
388
|
+
const awsConfigResponse = await this.setupAWSConfiguration({
|
|
389
|
+
is_shared_worker: true,
|
|
390
|
+
});
|
|
391
|
+
if (!awsConfigResponse) {
|
|
392
|
+
throw new Error('Failed to configure AWS');
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
await this.uploadAudioChunkInWorker({ audioFrames, fileName, chunkIndex });
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
console.error('Error uploading audio to S3: uploadAudioToS3WithWorker: ', error);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async uploadAudioToS3WithoutWorker({ audioFrames, fileName, chunkIndex, }) {
|
|
402
|
+
try {
|
|
403
|
+
if (!this.isAWSConfigured) {
|
|
404
|
+
const awsConfigResponse = await this.setupAWSConfiguration({
|
|
405
|
+
is_shared_worker: false,
|
|
406
|
+
});
|
|
407
|
+
if (!awsConfigResponse) {
|
|
408
|
+
throw new Error('Failed to configure AWS');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
await this.uploadAudioChunkInMain({ audioFrames, fileName, chunkIndex });
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
console.error('Error uploading audio to S3:', error);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Download audio as a file to the user's device (for debugging)
|
|
419
|
+
*/
|
|
420
|
+
downloadAudio(audioBlob, fileName) {
|
|
421
|
+
const url = URL.createObjectURL(audioBlob);
|
|
422
|
+
const a = document.createElement('a');
|
|
423
|
+
a.style.display = 'none';
|
|
424
|
+
a.href = url;
|
|
425
|
+
a.download = fileName;
|
|
426
|
+
document.body.appendChild(a);
|
|
427
|
+
a.click();
|
|
428
|
+
URL.revokeObjectURL(url);
|
|
429
|
+
document.body.removeChild(a);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Wait for all upload promises to complete
|
|
433
|
+
*/
|
|
434
|
+
async waitForAllUploads() {
|
|
435
|
+
if (this.sharedWorkerInstance) {
|
|
436
|
+
return new Promise((resolve, reject) => {
|
|
437
|
+
// one-time message handler to listen for the response
|
|
438
|
+
const messageHandler = (event) => {
|
|
439
|
+
if (event.data.action === SHARED_WORKER_ACTION.WAIT_FOR_ALL_UPLOADS_SUCCESS) {
|
|
440
|
+
this.sharedWorkerInstance?.port.removeEventListener('message', messageHandler);
|
|
441
|
+
resolve();
|
|
442
|
+
}
|
|
443
|
+
else if (event.data.action === SHARED_WORKER_ACTION.WAIT_FOR_ALL_UPLOADS_ERROR) {
|
|
444
|
+
const { uploadRequestReceived } = event.data.response || {};
|
|
445
|
+
if (uploadRequestReceived === 0) {
|
|
446
|
+
this.sharedWorkerInstance?.port.removeEventListener('message', messageHandler);
|
|
447
|
+
reject();
|
|
448
|
+
}
|
|
449
|
+
this.sharedWorkerInstance?.port.postMessage({
|
|
450
|
+
action: SHARED_WORKER_ACTION.WAIT_FOR_ALL_UPLOADS,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
// one-time listener
|
|
455
|
+
this.sharedWorkerInstance?.port.addEventListener('message', messageHandler);
|
|
456
|
+
this.sharedWorkerInstance?.port.postMessage({
|
|
457
|
+
action: SHARED_WORKER_ACTION.WAIT_FOR_ALL_UPLOADS,
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
await Promise.allSettled(this.uploadPromises);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Get list of successfully uploaded files
|
|
467
|
+
*/
|
|
468
|
+
getSuccessfulUploads() {
|
|
469
|
+
return [...this.successfulUploads];
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Get list of all failed files
|
|
473
|
+
*/
|
|
474
|
+
getFailedUploads() {
|
|
475
|
+
const failedUploads = [];
|
|
476
|
+
this.audioChunks.forEach((chunk) => {
|
|
477
|
+
if (chunk.status != 'success') {
|
|
478
|
+
failedUploads.push(chunk.fileName);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
return failedUploads;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Get list of all audio chunks
|
|
485
|
+
*/
|
|
486
|
+
getTotalAudioChunks() {
|
|
487
|
+
return this.audioChunks;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Retry uploading failed files
|
|
491
|
+
*/
|
|
492
|
+
async retryFailedUploads() {
|
|
493
|
+
const failedFiles = this.getFailedUploads();
|
|
494
|
+
if (failedFiles.length === 0) {
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
if (this.sharedWorkerInstance) {
|
|
498
|
+
this.audioChunks.forEach((chunk, index) => {
|
|
499
|
+
const { fileName, fileBlob, status, audioFrames } = chunk;
|
|
500
|
+
if (status != 'success') {
|
|
501
|
+
this.sharedWorkerInstance?.port.postMessage({
|
|
502
|
+
action: SHARED_WORKER_ACTION.UPLOAD_FILE_WITH_WORKER,
|
|
503
|
+
payload: {
|
|
504
|
+
audioFrames,
|
|
505
|
+
fileBlob,
|
|
506
|
+
fileName: `${this.filePath}/${fileName}`,
|
|
507
|
+
txnID: this.txnID,
|
|
508
|
+
businessID: this.businessID,
|
|
509
|
+
chunkIndex: index,
|
|
510
|
+
fileCount: fileName,
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
this.uploadPromises = []; // Reset upload promises for retries
|
|
518
|
+
if (this.onProgressCallback) {
|
|
519
|
+
this.onProgressCallback({
|
|
520
|
+
success: this.successfulUploads.length,
|
|
521
|
+
total: this.audioChunks.length,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
this.audioChunks.forEach((chunk, index) => {
|
|
525
|
+
const { fileName, fileBlob, status, audioFrames } = chunk;
|
|
526
|
+
if (status != 'success') {
|
|
527
|
+
let failedFileBlob;
|
|
528
|
+
if (status === 'failure') {
|
|
529
|
+
failedFileBlob = fileBlob;
|
|
530
|
+
}
|
|
531
|
+
if (status === 'pending') {
|
|
532
|
+
const compressedAudioBuffer = compressAudioToMp3(audioFrames);
|
|
533
|
+
failedFileBlob = new Blob(compressedAudioBuffer, {
|
|
534
|
+
type: AUDIO_EXTENSION_TYPE_MAP[OUTPUT_FORMAT],
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
if (failedFileBlob) {
|
|
538
|
+
const uploadPromise = pushFileToS3({
|
|
539
|
+
fileBlob: failedFileBlob,
|
|
540
|
+
fileName: `${this.filePath}/${fileName}`,
|
|
541
|
+
txnID: this.txnID,
|
|
542
|
+
businessID: this.businessID,
|
|
543
|
+
is_shared_worker: false,
|
|
544
|
+
}).then((response) => {
|
|
545
|
+
if (response.success) {
|
|
546
|
+
this.successfulUploads.push(fileName);
|
|
547
|
+
if (this.onProgressCallback) {
|
|
548
|
+
this.onProgressCallback({
|
|
549
|
+
success: this.successfulUploads.length,
|
|
550
|
+
total: this.audioChunks.length,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
this.audioChunks[index] = {
|
|
554
|
+
...this.audioChunks[index],
|
|
555
|
+
audioFrames: undefined,
|
|
556
|
+
fileBlob: undefined,
|
|
557
|
+
status: 'success',
|
|
558
|
+
response: response.success,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
return response;
|
|
562
|
+
});
|
|
563
|
+
this.uploadPromises.push(uploadPromise);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
// Wait for all retry promises to complete
|
|
569
|
+
await this.waitForAllUploads();
|
|
570
|
+
return this.getFailedUploads();
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Reset the upload state
|
|
574
|
+
*/
|
|
575
|
+
resetFileManagerInstance() {
|
|
576
|
+
this.initialiseClassInstance();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
export default AudioFileManager;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { MicVAD } from '@ricky0123/vad-web';
|
|
2
|
+
declare class VadWebClient {
|
|
3
|
+
private vad_past;
|
|
4
|
+
private last_clip_index;
|
|
5
|
+
private clip_points;
|
|
6
|
+
private sil_duration_acc;
|
|
7
|
+
private pref_length_samples;
|
|
8
|
+
private desp_length_samples;
|
|
9
|
+
private max_length_samples;
|
|
10
|
+
private shor_thsld;
|
|
11
|
+
private long_thsld;
|
|
12
|
+
private frame_size;
|
|
13
|
+
private speech_pad_frames;
|
|
14
|
+
private micVad;
|
|
15
|
+
private is_vad_loading;
|
|
16
|
+
private noSpeechStartTime;
|
|
17
|
+
private recording_started;
|
|
18
|
+
private lastWarningTime;
|
|
19
|
+
private warningCooldownPeriod;
|
|
20
|
+
/**
|
|
21
|
+
* Class that handle Vad functions and manages audio chunk
|
|
22
|
+
* @param pref_length Preferred length in seconds
|
|
23
|
+
* @param desp_length Desperate length in seconds
|
|
24
|
+
* @param max_length Maximum length in seconds
|
|
25
|
+
* @param sr Sample rate in Hz
|
|
26
|
+
*/
|
|
27
|
+
constructor(pref_length: number, desp_length: number, max_length: number, sr: number);
|
|
28
|
+
/**
|
|
29
|
+
* Check for continuous silence and trigger periodic warnings
|
|
30
|
+
* @param isSpeech - vad probability (0 or 1)
|
|
31
|
+
*/
|
|
32
|
+
checkNoSpeech(isSpeech: number): void;
|
|
33
|
+
/**
|
|
34
|
+
* Get the micVad instance
|
|
35
|
+
*/
|
|
36
|
+
getMicVad(): MicVAD;
|
|
37
|
+
/**
|
|
38
|
+
* Check if VAD is loading
|
|
39
|
+
*/
|
|
40
|
+
isVadLoading(): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Process a VAD frame and determine if it's a clip point
|
|
43
|
+
* @param vad_frame Voice activity detection frame (0 for silence, 1 for speech)
|
|
44
|
+
*/
|
|
45
|
+
processVadFrame(vad_frame: number): [boolean, number];
|
|
46
|
+
/**
|
|
47
|
+
* initialize the VAD instance
|
|
48
|
+
*/
|
|
49
|
+
initVad(): Promise<boolean>;
|
|
50
|
+
/**
|
|
51
|
+
* reinitialize the vad instance
|
|
52
|
+
*/
|
|
53
|
+
reinitializeVad(): Promise<boolean>;
|
|
54
|
+
/**
|
|
55
|
+
* process and upload audio chunk to s3
|
|
56
|
+
*/
|
|
57
|
+
processAudioChunk({ audioFrames }: {
|
|
58
|
+
audioFrames?: Float32Array;
|
|
59
|
+
}): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Start VAD
|
|
62
|
+
*/
|
|
63
|
+
startVad(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Pause VAD
|
|
66
|
+
*/
|
|
67
|
+
pauseVad(): void;
|
|
68
|
+
/**
|
|
69
|
+
* reset vadWeb instance
|
|
70
|
+
*/
|
|
71
|
+
resetVadWebInstance(): void;
|
|
72
|
+
/**
|
|
73
|
+
* monitor initial audio capture within starting 4 seconds
|
|
74
|
+
*/
|
|
75
|
+
monitorAudioCapture(): void;
|
|
76
|
+
/**
|
|
77
|
+
* Callback to configure constants
|
|
78
|
+
*/
|
|
79
|
+
configureVadConstants({ pref_length, desp_length, max_length, sr, frame_size, pre_speech_pad_frames, short_thsld, long_thsld, }: {
|
|
80
|
+
pref_length: number;
|
|
81
|
+
desp_length: number;
|
|
82
|
+
max_length: number;
|
|
83
|
+
sr: number;
|
|
84
|
+
frame_size: number;
|
|
85
|
+
pre_speech_pad_frames: number;
|
|
86
|
+
short_thsld: number;
|
|
87
|
+
long_thsld: number;
|
|
88
|
+
}): void;
|
|
89
|
+
}
|
|
90
|
+
export default VadWebClient;
|