@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.
@@ -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}));
@@ -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
+ }