@sridhar-mani/whisper-web-transcriber 0.1.0
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/LICENSE +21 -0
- package/README.md +323 -0
- package/dist/index.bundled.js +441 -0
- package/dist/index.bundled.min.js +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.esm.js +433 -0
- package/dist/index.js +441 -0
- package/dist/index.min.js +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
(function (global, factory) {
|
|
2
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
3
|
+
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
4
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.WhisperTranscriber = {}));
|
|
5
|
+
})(this, (function (exports) { 'use strict';
|
|
6
|
+
|
|
7
|
+
class WhisperTranscriber {
|
|
8
|
+
constructor(config = {}) {
|
|
9
|
+
this.instance = null;
|
|
10
|
+
this.mediaRecorder = null;
|
|
11
|
+
this.audioContext = null;
|
|
12
|
+
this.isRecording = false;
|
|
13
|
+
this.audio = null;
|
|
14
|
+
this.audio0 = null;
|
|
15
|
+
this.Module = null;
|
|
16
|
+
this.modelLoaded = false;
|
|
17
|
+
this.initPromise = null;
|
|
18
|
+
this.config = {
|
|
19
|
+
modelUrl: config.modelUrl || WhisperTranscriber.MODEL_URLS[config.modelSize || 'base-en-q5_1'],
|
|
20
|
+
modelSize: config.modelSize || 'base-en-q5_1',
|
|
21
|
+
sampleRate: config.sampleRate || 16000,
|
|
22
|
+
audioIntervalMs: config.audioIntervalMs || 5000,
|
|
23
|
+
onTranscription: config.onTranscription || (() => { }),
|
|
24
|
+
onProgress: config.onProgress || (() => { }),
|
|
25
|
+
onStatus: config.onStatus || (() => { }),
|
|
26
|
+
debug: config.debug || false,
|
|
27
|
+
};
|
|
28
|
+
// Auto-register COI service worker if needed
|
|
29
|
+
this.registerServiceWorkerIfNeeded();
|
|
30
|
+
}
|
|
31
|
+
log(message) {
|
|
32
|
+
if (this.config.debug) {
|
|
33
|
+
console.log('[WhisperTranscriber]', message);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async registerServiceWorkerIfNeeded() {
|
|
37
|
+
// Check if we need COI and service worker is available
|
|
38
|
+
if (!window.crossOriginIsolated) {
|
|
39
|
+
// For CDN usage, we cannot auto-register service workers due to same-origin policy
|
|
40
|
+
// Instead, provide instructions or helper method
|
|
41
|
+
if (window.COI_SERVICEWORKER_CODE) {
|
|
42
|
+
console.warn('[WhisperTranscriber] SharedArrayBuffer is not available. ' +
|
|
43
|
+
'To enable it, you need to serve your site with COOP/COEP headers or use a service worker.\n' +
|
|
44
|
+
'You can get the service worker code by calling: transcriber.getServiceWorkerCode()');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Returns the COI service worker code that users need to save and serve from their domain
|
|
50
|
+
*/
|
|
51
|
+
getServiceWorkerCode() {
|
|
52
|
+
if (window.COI_SERVICEWORKER_CODE) {
|
|
53
|
+
return window.COI_SERVICEWORKER_CODE;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Helper to generate instructions for setting up Cross-Origin Isolation
|
|
59
|
+
*/
|
|
60
|
+
getCrossOriginIsolationInstructions() {
|
|
61
|
+
const swCode = this.getServiceWorkerCode();
|
|
62
|
+
if (!window.crossOriginIsolated) {
|
|
63
|
+
return `
|
|
64
|
+
Cross-Origin Isolation Setup Required
|
|
65
|
+
=====================================
|
|
66
|
+
|
|
67
|
+
WhisperTranscriber requires SharedArrayBuffer, which needs Cross-Origin Isolation.
|
|
68
|
+
|
|
69
|
+
Option 1: Server Headers (Recommended)
|
|
70
|
+
--------------------------------------
|
|
71
|
+
Configure your server to send these headers:
|
|
72
|
+
Cross-Origin-Embedder-Policy: require-corp
|
|
73
|
+
Cross-Origin-Opener-Policy: same-origin
|
|
74
|
+
|
|
75
|
+
Option 2: Service Worker
|
|
76
|
+
------------------------
|
|
77
|
+
1. Save the following code as 'coi-serviceworker.js' in your website root:
|
|
78
|
+
|
|
79
|
+
${swCode ? '--- START SERVICE WORKER CODE ---\n' + swCode + '\n--- END SERVICE WORKER CODE ---' : '[Service worker code not available]'}
|
|
80
|
+
|
|
81
|
+
2. Register the service worker by adding this to your HTML:
|
|
82
|
+
<script src="/coi-serviceworker.js"></script>
|
|
83
|
+
|
|
84
|
+
3. Reload the page after registration.
|
|
85
|
+
|
|
86
|
+
Current Status:
|
|
87
|
+
- crossOriginIsolated: ${window.crossOriginIsolated}
|
|
88
|
+
- SharedArrayBuffer available: ${typeof SharedArrayBuffer !== 'undefined'}
|
|
89
|
+
`.trim();
|
|
90
|
+
}
|
|
91
|
+
return 'Cross-Origin Isolation is already enabled! No action needed.';
|
|
92
|
+
}
|
|
93
|
+
getScriptBasePath() {
|
|
94
|
+
// Always use local src/ directory for all assets
|
|
95
|
+
return '/src/';
|
|
96
|
+
}
|
|
97
|
+
async createWorkerFromURL(url) {
|
|
98
|
+
// Fetch the worker script
|
|
99
|
+
const response = await fetch(url);
|
|
100
|
+
const workerCode = await response.text();
|
|
101
|
+
// Create a blob URL for the worker
|
|
102
|
+
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
103
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
104
|
+
return new Worker(blobUrl);
|
|
105
|
+
}
|
|
106
|
+
async loadWasmModule() {
|
|
107
|
+
// Check if we have inlined worker code
|
|
108
|
+
if (window.LIBSTREAM_WORKER_CODE) {
|
|
109
|
+
// Use inlined worker
|
|
110
|
+
this.log('Using inlined worker code');
|
|
111
|
+
const workerBlob = new Blob([window.LIBSTREAM_WORKER_CODE], { type: 'application/javascript' });
|
|
112
|
+
const workerBlobUrl = URL.createObjectURL(workerBlob);
|
|
113
|
+
window.__whisperWorkerBlobUrl = workerBlobUrl;
|
|
114
|
+
this.log('Worker blob URL created from inlined code');
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// Fallback to fetching worker
|
|
118
|
+
const basePath = this.getScriptBasePath();
|
|
119
|
+
const workerUrl = basePath + 'libstream.worker.js';
|
|
120
|
+
try {
|
|
121
|
+
// Pre-fetch and convert worker to blob URL
|
|
122
|
+
const response = await fetch(workerUrl);
|
|
123
|
+
const workerCode = await response.text();
|
|
124
|
+
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
125
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
126
|
+
// Store the blob URL for later use
|
|
127
|
+
window.__whisperWorkerBlobUrl = blobUrl;
|
|
128
|
+
this.log('Worker script loaded and blob URL created');
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
this.log('Failed to pre-fetch worker: ' + error);
|
|
132
|
+
// Continue anyway, it might work with direct loading
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
// Configure Module before the script loads
|
|
137
|
+
window.Module = {
|
|
138
|
+
locateFile: (path) => {
|
|
139
|
+
// If it's the worker and we have a blob URL, use it
|
|
140
|
+
if (path === 'libstream.worker.js' && window.__whisperWorkerBlobUrl) {
|
|
141
|
+
return window.__whisperWorkerBlobUrl;
|
|
142
|
+
}
|
|
143
|
+
return this.getScriptBasePath() + path;
|
|
144
|
+
},
|
|
145
|
+
onRuntimeInitialized: () => {
|
|
146
|
+
this.log('WASM runtime initialized');
|
|
147
|
+
// The runtime is initialized, we can resolve immediately
|
|
148
|
+
// The Module will set up the whisper functions
|
|
149
|
+
setTimeout(() => {
|
|
150
|
+
const module = window.Module;
|
|
151
|
+
if (module) {
|
|
152
|
+
this.Module = module;
|
|
153
|
+
// Set up the whisper functions if they don't exist
|
|
154
|
+
if (!module.init) {
|
|
155
|
+
module.init = module.cwrap('init', 'number', ['string']);
|
|
156
|
+
}
|
|
157
|
+
if (!module.set_audio) {
|
|
158
|
+
module.set_audio = module.cwrap('set_audio', '', ['number', 'array']);
|
|
159
|
+
}
|
|
160
|
+
if (!module.get_transcribed) {
|
|
161
|
+
module.get_transcribed = module.cwrap('get_transcribed', 'string', []);
|
|
162
|
+
}
|
|
163
|
+
if (!module.set_status) {
|
|
164
|
+
module.set_status = module.cwrap('set_status', '', ['string']);
|
|
165
|
+
}
|
|
166
|
+
this.log('WASM module loaded and functions initialized');
|
|
167
|
+
resolve();
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
reject(new Error('Module not available after runtime initialized'));
|
|
171
|
+
}
|
|
172
|
+
}, 100);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
// Load the WASM module
|
|
176
|
+
if (window.LIBSTREAM_CODE) {
|
|
177
|
+
// Use inlined libstream code
|
|
178
|
+
this.log('Using inlined libstream code');
|
|
179
|
+
const scriptBlob = new Blob([window.LIBSTREAM_CODE], { type: 'application/javascript' });
|
|
180
|
+
const scriptUrl = URL.createObjectURL(scriptBlob);
|
|
181
|
+
const script = document.createElement('script');
|
|
182
|
+
script.src = scriptUrl;
|
|
183
|
+
script.onerror = () => reject(new Error('Failed to load WASM module'));
|
|
184
|
+
document.head.appendChild(script);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// Load the WASM module dynamically
|
|
188
|
+
const script = document.createElement('script');
|
|
189
|
+
script.src = this.getScriptBasePath() + 'libstream.js';
|
|
190
|
+
script.onerror = () => reject(new Error('Failed to load WASM module'));
|
|
191
|
+
document.head.appendChild(script);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
async loadHelpers() {
|
|
196
|
+
if (window.HELPERS_CODE) {
|
|
197
|
+
// Use inlined helpers code
|
|
198
|
+
this.log('Using inlined helpers code');
|
|
199
|
+
const scriptBlob = new Blob([window.HELPERS_CODE], { type: 'application/javascript' });
|
|
200
|
+
const scriptUrl = URL.createObjectURL(scriptBlob);
|
|
201
|
+
const script = document.createElement('script');
|
|
202
|
+
script.src = scriptUrl;
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
script.onload = () => resolve();
|
|
205
|
+
script.onerror = () => reject(new Error('Failed to load helpers'));
|
|
206
|
+
document.head.appendChild(script);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// Load helpers.js normally
|
|
211
|
+
const script = document.createElement('script');
|
|
212
|
+
script.src = this.getScriptBasePath() + 'helpers.js';
|
|
213
|
+
return new Promise((resolve, reject) => {
|
|
214
|
+
script.onload = () => resolve();
|
|
215
|
+
script.onerror = () => reject(new Error('Failed to load helpers'));
|
|
216
|
+
document.head.appendChild(script);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async loadCOIServiceWorker() {
|
|
221
|
+
// Check if SharedArrayBuffer is already available
|
|
222
|
+
if (typeof SharedArrayBuffer !== 'undefined') {
|
|
223
|
+
this.log('SharedArrayBuffer already available');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
// Try to load coi-serviceworker.js
|
|
227
|
+
const basePath = this.getScriptBasePath();
|
|
228
|
+
const script = document.createElement('script');
|
|
229
|
+
script.src = basePath + 'coi-serviceworker.js';
|
|
230
|
+
return new Promise((resolve) => {
|
|
231
|
+
script.onload = () => {
|
|
232
|
+
this.log('COI service worker loaded');
|
|
233
|
+
resolve();
|
|
234
|
+
};
|
|
235
|
+
script.onerror = () => {
|
|
236
|
+
this.log('Failed to load COI service worker - SharedArrayBuffer may not be available');
|
|
237
|
+
resolve(); // Continue anyway
|
|
238
|
+
};
|
|
239
|
+
document.head.appendChild(script);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
async initialize() {
|
|
243
|
+
if (this.initPromise) {
|
|
244
|
+
return this.initPromise;
|
|
245
|
+
}
|
|
246
|
+
this.initPromise = (async () => {
|
|
247
|
+
try {
|
|
248
|
+
// Try to load COI service worker first for SharedArrayBuffer support
|
|
249
|
+
await this.loadCOIServiceWorker();
|
|
250
|
+
// Set up global variables required by helpers.js
|
|
251
|
+
window.dbVersion = 1;
|
|
252
|
+
window.dbName = 'whisper.transcriber.models';
|
|
253
|
+
// Don't override indexedDB, it's already a global property
|
|
254
|
+
// Load helpers first
|
|
255
|
+
await this.loadHelpers();
|
|
256
|
+
this.log('Helpers loaded');
|
|
257
|
+
// Then load WASM module
|
|
258
|
+
await this.loadWasmModule();
|
|
259
|
+
this.log('WASM module initialized');
|
|
260
|
+
this.config.onStatus('Ready to load model');
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
this.log('Failed to initialize: ' + error);
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
})();
|
|
267
|
+
return this.initPromise;
|
|
268
|
+
}
|
|
269
|
+
async loadModel() {
|
|
270
|
+
if (this.modelLoaded) {
|
|
271
|
+
this.log('Model already loaded');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
await this.initialize();
|
|
275
|
+
return new Promise((resolve, reject) => {
|
|
276
|
+
const url = this.config.modelUrl;
|
|
277
|
+
const size_mb = WhisperTranscriber.MODEL_SIZES[this.config.modelSize];
|
|
278
|
+
this.config.onStatus('Loading model...');
|
|
279
|
+
const storeFS = (fname, buf) => {
|
|
280
|
+
try {
|
|
281
|
+
this.Module.FS_unlink(fname);
|
|
282
|
+
}
|
|
283
|
+
catch (e) {
|
|
284
|
+
// File doesn't exist, ignore
|
|
285
|
+
}
|
|
286
|
+
this.Module.FS_createDataFile("/", fname, buf, true, true);
|
|
287
|
+
this.log(`Model stored: ${fname}, size: ${buf.length}`);
|
|
288
|
+
this.modelLoaded = true;
|
|
289
|
+
this.config.onStatus('Model loaded successfully');
|
|
290
|
+
resolve();
|
|
291
|
+
};
|
|
292
|
+
const cbProgress = (progress) => {
|
|
293
|
+
this.config.onProgress(Math.round(progress * 100));
|
|
294
|
+
};
|
|
295
|
+
const cbCancel = () => {
|
|
296
|
+
this.config.onStatus('Model loading cancelled');
|
|
297
|
+
reject(new Error('Model loading cancelled'));
|
|
298
|
+
};
|
|
299
|
+
const cbPrint = (msg) => {
|
|
300
|
+
this.log(msg);
|
|
301
|
+
};
|
|
302
|
+
// Use the global loadRemote function from helpers.js
|
|
303
|
+
window.loadRemote(url, 'whisper.bin', size_mb, cbProgress, storeFS, cbCancel, cbPrint);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
async startRecording() {
|
|
307
|
+
if (!this.modelLoaded) {
|
|
308
|
+
throw new Error('Model not loaded. Call loadModel() first.');
|
|
309
|
+
}
|
|
310
|
+
if (this.isRecording) {
|
|
311
|
+
this.log('Already recording');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
// Initialize whisper instance
|
|
315
|
+
if (!this.instance) {
|
|
316
|
+
// Check if init function exists, otherwise use cwrap
|
|
317
|
+
const init = this.Module.init || this.Module.cwrap('init', 'number', ['string']);
|
|
318
|
+
this.instance = init('whisper.bin');
|
|
319
|
+
if (!this.instance) {
|
|
320
|
+
throw new Error('Failed to initialize Whisper');
|
|
321
|
+
}
|
|
322
|
+
this.log('Whisper instance initialized');
|
|
323
|
+
}
|
|
324
|
+
// Create audio context
|
|
325
|
+
this.audioContext = new AudioContext({
|
|
326
|
+
sampleRate: this.config.sampleRate,
|
|
327
|
+
// @ts-ignore - These properties might not be in the type definition
|
|
328
|
+
channelCount: 1,
|
|
329
|
+
echoCancellation: false,
|
|
330
|
+
autoGainControl: true,
|
|
331
|
+
noiseSuppression: true,
|
|
332
|
+
});
|
|
333
|
+
const set_status = this.Module.set_status || this.Module.cwrap('set_status', '', ['string']);
|
|
334
|
+
set_status("");
|
|
335
|
+
this.isRecording = true;
|
|
336
|
+
this.config.onStatus('Recording...');
|
|
337
|
+
const chunks = [];
|
|
338
|
+
try {
|
|
339
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
|
|
340
|
+
this.mediaRecorder = new MediaRecorder(stream);
|
|
341
|
+
this.mediaRecorder.ondataavailable = (e) => {
|
|
342
|
+
chunks.push(e.data);
|
|
343
|
+
const blob = new Blob(chunks, { type: 'audio/ogg; codecs=opus' });
|
|
344
|
+
const reader = new FileReader();
|
|
345
|
+
reader.onload = (event) => {
|
|
346
|
+
const buf = new Uint8Array(event.target.result);
|
|
347
|
+
if (!this.audioContext)
|
|
348
|
+
return;
|
|
349
|
+
this.audioContext.decodeAudioData(buf.buffer, (audioBuffer) => {
|
|
350
|
+
const offlineContext = new OfflineAudioContext(audioBuffer.numberOfChannels, audioBuffer.length, audioBuffer.sampleRate);
|
|
351
|
+
const source = offlineContext.createBufferSource();
|
|
352
|
+
source.buffer = audioBuffer;
|
|
353
|
+
source.connect(offlineContext.destination);
|
|
354
|
+
source.start(0);
|
|
355
|
+
offlineContext.startRendering().then((renderedBuffer) => {
|
|
356
|
+
this.audio = renderedBuffer.getChannelData(0);
|
|
357
|
+
const audioAll = new Float32Array(this.audio0 == null ? this.audio.length : this.audio0.length + this.audio.length);
|
|
358
|
+
if (this.audio0 != null) {
|
|
359
|
+
audioAll.set(this.audio0, 0);
|
|
360
|
+
}
|
|
361
|
+
audioAll.set(this.audio, this.audio0 == null ? 0 : this.audio0.length);
|
|
362
|
+
if (this.instance) {
|
|
363
|
+
const set_audio = this.Module.set_audio || this.Module.cwrap('set_audio', '', ['number', 'array']);
|
|
364
|
+
set_audio(this.instance, audioAll);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
};
|
|
369
|
+
reader.readAsArrayBuffer(blob);
|
|
370
|
+
};
|
|
371
|
+
this.mediaRecorder.onstop = () => {
|
|
372
|
+
if (this.isRecording) {
|
|
373
|
+
setTimeout(() => this.startRecording(), 0);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
this.mediaRecorder.start(this.config.audioIntervalMs);
|
|
377
|
+
// Start transcription polling
|
|
378
|
+
this.startTranscriptionPolling();
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
this.isRecording = false;
|
|
382
|
+
this.config.onStatus('Error: ' + error.message);
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
startTranscriptionPolling() {
|
|
387
|
+
const interval = setInterval(() => {
|
|
388
|
+
if (!this.isRecording) {
|
|
389
|
+
clearInterval(interval);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const get_transcribed = this.Module.get_transcribed || this.Module.cwrap('get_transcribed', 'string', []);
|
|
393
|
+
const transcribed = get_transcribed();
|
|
394
|
+
if (transcribed != null && transcribed.length > 1) {
|
|
395
|
+
this.config.onTranscription(transcribed);
|
|
396
|
+
}
|
|
397
|
+
}, 100);
|
|
398
|
+
}
|
|
399
|
+
stopRecording() {
|
|
400
|
+
if (!this.isRecording) {
|
|
401
|
+
this.log('Not recording');
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const set_status = this.Module.set_status || this.Module.cwrap('set_status', '', ['string']);
|
|
405
|
+
set_status("paused");
|
|
406
|
+
this.isRecording = false;
|
|
407
|
+
this.audio0 = null;
|
|
408
|
+
this.audio = null;
|
|
409
|
+
if (this.mediaRecorder) {
|
|
410
|
+
this.mediaRecorder.stop();
|
|
411
|
+
this.mediaRecorder = null;
|
|
412
|
+
}
|
|
413
|
+
if (this.audioContext) {
|
|
414
|
+
this.audioContext.close();
|
|
415
|
+
this.audioContext = null;
|
|
416
|
+
}
|
|
417
|
+
this.config.onStatus('Stopped');
|
|
418
|
+
}
|
|
419
|
+
destroy() {
|
|
420
|
+
this.stopRecording();
|
|
421
|
+
this.instance = null;
|
|
422
|
+
this.Module = null;
|
|
423
|
+
this.modelLoaded = false;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
WhisperTranscriber.MODEL_URLS = {
|
|
427
|
+
'tiny.en': 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.en.bin',
|
|
428
|
+
'base.en': 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin',
|
|
429
|
+
'tiny-en-q5_1': 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.en-q5_1.bin',
|
|
430
|
+
'base-en-q5_1': 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en-q5_1.bin',
|
|
431
|
+
};
|
|
432
|
+
WhisperTranscriber.MODEL_SIZES = {
|
|
433
|
+
'tiny.en': 75,
|
|
434
|
+
'base.en': 142,
|
|
435
|
+
'tiny-en-q5_1': 31,
|
|
436
|
+
'base-en-q5_1': 57,
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
exports.WhisperTranscriber = WhisperTranscriber;
|
|
440
|
+
|
|
441
|
+
}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
!function(e,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i((e="undefined"!=typeof globalThis?globalThis:e||self).WhisperTranscriber={})}(this,(function(e){"use strict";class i{constructor(e={}){this.instance=null,this.mediaRecorder=null,this.audioContext=null,this.isRecording=!1,this.audio=null,this.audio0=null,this.Module=null,this.modelLoaded=!1,this.initPromise=null,this.config={modelUrl:e.modelUrl||i.MODEL_URLS[e.modelSize||"base-en-q5_1"],modelSize:e.modelSize||"base-en-q5_1",sampleRate:e.sampleRate||16e3,audioIntervalMs:e.audioIntervalMs||5e3,onTranscription:e.onTranscription||(()=>{}),onProgress:e.onProgress||(()=>{}),onStatus:e.onStatus||(()=>{}),debug:e.debug||!1},this.registerServiceWorkerIfNeeded()}log(e){this.config.debug&&console.log("[WhisperTranscriber]",e)}async registerServiceWorkerIfNeeded(){window.crossOriginIsolated||window.COI_SERVICEWORKER_CODE&&console.warn("[WhisperTranscriber] SharedArrayBuffer is not available. To enable it, you need to serve your site with COOP/COEP headers or use a service worker.\nYou can get the service worker code by calling: transcriber.getServiceWorkerCode()")}getServiceWorkerCode(){return window.COI_SERVICEWORKER_CODE?window.COI_SERVICEWORKER_CODE:null}getCrossOriginIsolationInstructions(){const e=this.getServiceWorkerCode();return window.crossOriginIsolated?"Cross-Origin Isolation is already enabled! No action needed.":`\nCross-Origin Isolation Setup Required\n=====================================\n\nWhisperTranscriber requires SharedArrayBuffer, which needs Cross-Origin Isolation.\n\nOption 1: Server Headers (Recommended)\n--------------------------------------\nConfigure your server to send these headers:\n Cross-Origin-Embedder-Policy: require-corp\n Cross-Origin-Opener-Policy: same-origin\n\nOption 2: Service Worker\n------------------------\n1. Save the following code as 'coi-serviceworker.js' in your website root:\n\n${e?"--- START SERVICE WORKER CODE ---\n"+e+"\n--- END SERVICE WORKER CODE ---":"[Service worker code not available]"}\n\n2. Register the service worker by adding this to your HTML:\n <script src="/coi-serviceworker.js"><\/script>\n\n3. Reload the page after registration.\n\nCurrent Status:\n- crossOriginIsolated: ${window.crossOriginIsolated}\n- SharedArrayBuffer available: ${"undefined"!=typeof SharedArrayBuffer}\n `.trim()}getScriptBasePath(){return"/src/"}async createWorkerFromURL(e){const i=await fetch(e),t=await i.text(),o=new Blob([t],{type:"application/javascript"}),r=URL.createObjectURL(o);return new Worker(r)}async loadWasmModule(){if(window.LIBSTREAM_WORKER_CODE){this.log("Using inlined worker code");const e=new Blob([window.LIBSTREAM_WORKER_CODE],{type:"application/javascript"}),i=URL.createObjectURL(e);window.__whisperWorkerBlobUrl=i,this.log("Worker blob URL created from inlined code")}else{const e=this.getScriptBasePath()+"libstream.worker.js";try{const i=await fetch(e),t=await i.text(),o=new Blob([t],{type:"application/javascript"}),r=URL.createObjectURL(o);window.__whisperWorkerBlobUrl=r,this.log("Worker script loaded and blob URL created")}catch(e){this.log("Failed to pre-fetch worker: "+e)}}return new Promise(((e,i)=>{if(window.Module={locateFile:e=>"libstream.worker.js"===e&&window.__whisperWorkerBlobUrl?window.__whisperWorkerBlobUrl:this.getScriptBasePath()+e,onRuntimeInitialized:()=>{this.log("WASM runtime initialized"),setTimeout((()=>{const t=window.Module;t?(this.Module=t,t.init||(t.init=t.cwrap("init","number",["string"])),t.set_audio||(t.set_audio=t.cwrap("set_audio","",["number","array"])),t.get_transcribed||(t.get_transcribed=t.cwrap("get_transcribed","string",[])),t.set_status||(t.set_status=t.cwrap("set_status","",["string"])),this.log("WASM module loaded and functions initialized"),e()):i(new Error("Module not available after runtime initialized"))}),100)}},window.LIBSTREAM_CODE){this.log("Using inlined libstream code");const e=new Blob([window.LIBSTREAM_CODE],{type:"application/javascript"}),t=URL.createObjectURL(e),o=document.createElement("script");o.src=t,o.onerror=()=>i(new Error("Failed to load WASM module")),document.head.appendChild(o)}else{const e=document.createElement("script");e.src=this.getScriptBasePath()+"libstream.js",e.onerror=()=>i(new Error("Failed to load WASM module")),document.head.appendChild(e)}}))}async loadHelpers(){if(window.HELPERS_CODE){this.log("Using inlined helpers code");const e=new Blob([window.HELPERS_CODE],{type:"application/javascript"}),i=URL.createObjectURL(e),t=document.createElement("script");return t.src=i,new Promise(((e,i)=>{t.onload=()=>e(),t.onerror=()=>i(new Error("Failed to load helpers")),document.head.appendChild(t)}))}{const e=document.createElement("script");return e.src=this.getScriptBasePath()+"helpers.js",new Promise(((i,t)=>{e.onload=()=>i(),e.onerror=()=>t(new Error("Failed to load helpers")),document.head.appendChild(e)}))}}async loadCOIServiceWorker(){if("undefined"!=typeof SharedArrayBuffer)return void this.log("SharedArrayBuffer already available");const e=this.getScriptBasePath(),i=document.createElement("script");return i.src=e+"coi-serviceworker.js",new Promise((e=>{i.onload=()=>{this.log("COI service worker loaded"),e()},i.onerror=()=>{this.log("Failed to load COI service worker - SharedArrayBuffer may not be available"),e()},document.head.appendChild(i)}))}async initialize(){return this.initPromise||(this.initPromise=(async()=>{try{await this.loadCOIServiceWorker(),window.dbVersion=1,window.dbName="whisper.transcriber.models",await this.loadHelpers(),this.log("Helpers loaded"),await this.loadWasmModule(),this.log("WASM module initialized"),this.config.onStatus("Ready to load model")}catch(e){throw this.log("Failed to initialize: "+e),e}})()),this.initPromise}async loadModel(){if(!this.modelLoaded)return await this.initialize(),new Promise(((e,t)=>{const o=this.config.modelUrl,r=i.MODEL_SIZES[this.config.modelSize];this.config.onStatus("Loading model...");window.loadRemote(o,"whisper.bin",r,(e=>{this.config.onProgress(Math.round(100*e))}),((i,t)=>{try{this.Module.FS_unlink(i)}catch(e){}this.Module.FS_createDataFile("/",i,t,!0,!0),this.log(`Model stored: ${i}, size: ${t.length}`),this.modelLoaded=!0,this.config.onStatus("Model loaded successfully"),e()}),(()=>{this.config.onStatus("Model loading cancelled"),t(new Error("Model loading cancelled"))}),(e=>{this.log(e)}))}));this.log("Model already loaded")}async startRecording(){if(!this.modelLoaded)throw new Error("Model not loaded. Call loadModel() first.");if(this.isRecording)return void this.log("Already recording");if(!this.instance){const e=this.Module.init||this.Module.cwrap("init","number",["string"]);if(this.instance=e("whisper.bin"),!this.instance)throw new Error("Failed to initialize Whisper");this.log("Whisper instance initialized")}this.audioContext=new AudioContext({sampleRate:this.config.sampleRate,channelCount:1,echoCancellation:!1,autoGainControl:!0,noiseSuppression:!0});(this.Module.set_status||this.Module.cwrap("set_status","",["string"]))(""),this.isRecording=!0,this.config.onStatus("Recording...");const e=[];try{const i=await navigator.mediaDevices.getUserMedia({audio:!0,video:!1});this.mediaRecorder=new MediaRecorder(i),this.mediaRecorder.ondataavailable=i=>{e.push(i.data);const t=new Blob(e,{type:"audio/ogg; codecs=opus"}),o=new FileReader;o.onload=e=>{const i=new Uint8Array(e.target.result);this.audioContext&&this.audioContext.decodeAudioData(i.buffer,(e=>{const i=new OfflineAudioContext(e.numberOfChannels,e.length,e.sampleRate),t=i.createBufferSource();t.buffer=e,t.connect(i.destination),t.start(0),i.startRendering().then((e=>{this.audio=e.getChannelData(0);const i=new Float32Array(null==this.audio0?this.audio.length:this.audio0.length+this.audio.length);if(null!=this.audio0&&i.set(this.audio0,0),i.set(this.audio,null==this.audio0?0:this.audio0.length),this.instance){(this.Module.set_audio||this.Module.cwrap("set_audio","",["number","array"]))(this.instance,i)}}))}))},o.readAsArrayBuffer(t)},this.mediaRecorder.onstop=()=>{this.isRecording&&setTimeout((()=>this.startRecording()),0)},this.mediaRecorder.start(this.config.audioIntervalMs),this.startTranscriptionPolling()}catch(e){throw this.isRecording=!1,this.config.onStatus("Error: "+e.message),e}}startTranscriptionPolling(){const e=setInterval((()=>{if(!this.isRecording)return void clearInterval(e);const i=(this.Module.get_transcribed||this.Module.cwrap("get_transcribed","string",[]))();null!=i&&i.length>1&&this.config.onTranscription(i)}),100)}stopRecording(){if(!this.isRecording)return void this.log("Not recording");(this.Module.set_status||this.Module.cwrap("set_status","",["string"]))("paused"),this.isRecording=!1,this.audio0=null,this.audio=null,this.mediaRecorder&&(this.mediaRecorder.stop(),this.mediaRecorder=null),this.audioContext&&(this.audioContext.close(),this.audioContext=null),this.config.onStatus("Stopped")}destroy(){this.stopRecording(),this.instance=null,this.Module=null,this.modelLoaded=!1}}i.MODEL_URLS={"tiny.en":"https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.en.bin","base.en":"https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin","tiny-en-q5_1":"https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.en-q5_1.bin","base-en-q5_1":"https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en-q5_1.bin"},i.MODEL_SIZES={"tiny.en":75,"base.en":142,"tiny-en-q5_1":31,"base-en-q5_1":57},e.WhisperTranscriber=i}));
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface WhisperConfig {
|
|
2
|
+
modelUrl?: string;
|
|
3
|
+
modelSize?: 'tiny.en' | 'base.en' | 'tiny-en-q5_1' | 'base-en-q5_1';
|
|
4
|
+
sampleRate?: number;
|
|
5
|
+
audioIntervalMs?: number;
|
|
6
|
+
onTranscription?: (text: string) => void;
|
|
7
|
+
onProgress?: (progress: number) => void;
|
|
8
|
+
onStatus?: (status: string) => void;
|
|
9
|
+
debug?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare class WhisperTranscriber {
|
|
12
|
+
private config;
|
|
13
|
+
private instance;
|
|
14
|
+
private mediaRecorder;
|
|
15
|
+
private audioContext;
|
|
16
|
+
private isRecording;
|
|
17
|
+
private audio;
|
|
18
|
+
private audio0;
|
|
19
|
+
private Module;
|
|
20
|
+
private modelLoaded;
|
|
21
|
+
private initPromise;
|
|
22
|
+
private static readonly MODEL_URLS;
|
|
23
|
+
private static readonly MODEL_SIZES;
|
|
24
|
+
constructor(config?: WhisperConfig);
|
|
25
|
+
private log;
|
|
26
|
+
private registerServiceWorkerIfNeeded;
|
|
27
|
+
/**
|
|
28
|
+
* Returns the COI service worker code that users need to save and serve from their domain
|
|
29
|
+
*/
|
|
30
|
+
getServiceWorkerCode(): string | null;
|
|
31
|
+
/**
|
|
32
|
+
* Helper to generate instructions for setting up Cross-Origin Isolation
|
|
33
|
+
*/
|
|
34
|
+
getCrossOriginIsolationInstructions(): string;
|
|
35
|
+
private getScriptBasePath;
|
|
36
|
+
private createWorkerFromURL;
|
|
37
|
+
private loadWasmModule;
|
|
38
|
+
private loadHelpers;
|
|
39
|
+
private loadCOIServiceWorker;
|
|
40
|
+
initialize(): Promise<void>;
|
|
41
|
+
loadModel(): Promise<void>;
|
|
42
|
+
startRecording(): Promise<void>;
|
|
43
|
+
private startTranscriptionPolling;
|
|
44
|
+
stopRecording(): void;
|
|
45
|
+
destroy(): void;
|
|
46
|
+
}
|