radames commited on
Commit
80da23b
1 Parent(s): 61643cb
Files changed (3) hide show
  1. helpers.js +183 -0
  2. index.html +554 -18
  3. main.js +0 -0
helpers.js ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Common Javascript functions used by the examples
2
+
3
+ function convertTypedArray(src, type) {
4
+ var buffer = new ArrayBuffer(src.byteLength);
5
+ var baseView = new src.constructor(buffer).set(src);
6
+ return new type(buffer);
7
+ }
8
+
9
+ var printTextarea = (function() {
10
+ var element = document.getElementById('output');
11
+ if (element) element.alue = ''; // clear browser cache
12
+ return function(text) {
13
+ if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
14
+ console.log(text);
15
+ if (element) {
16
+ element.value += text + "\n";
17
+ element.scrollTop = element.scrollHeight; // focus on bottom
18
+ }
19
+ };
20
+ })();
21
+
22
+ async function clearCache() {
23
+ if (confirm('Are you sure you want to clear the cache?\nAll the models will be downloaded again.')) {
24
+ indexedDB.deleteDatabase(dbName);
25
+ }
26
+ }
27
+
28
+ // fetch a remote file from remote URL using the Fetch API
29
+ async function fetchRemote(url, cbProgress, cbPrint) {
30
+ cbPrint('fetchRemote: downloading with fetch()...');
31
+
32
+ const response = await fetch(
33
+ url,
34
+ {
35
+ method: 'GET',
36
+ headers: {
37
+ 'Content-Type': 'application/octet-stream',
38
+ },
39
+ mode: 'no-cors'
40
+ }
41
+ );
42
+
43
+ if (!response.ok) {
44
+ cbPrint('fetchRemote: failed to fetch ' + url);
45
+ return;
46
+ }
47
+
48
+ const contentLength = response.headers.get('content-length');
49
+ const total = parseInt(contentLength, 10);
50
+ const reader = response.body.getReader();
51
+
52
+ var chunks = [];
53
+ var receivedLength = 0;
54
+ var progressLast = -1;
55
+
56
+ while (true) {
57
+ const { done, value } = await reader.read();
58
+
59
+ if (done) {
60
+ break;
61
+ }
62
+
63
+ chunks.push(value);
64
+ receivedLength += value.length;
65
+
66
+ if (contentLength) {
67
+ cbProgress(receivedLength/total);
68
+
69
+ var progressCur = Math.round((receivedLength / total) * 10);
70
+ if (progressCur != progressLast) {
71
+ cbPrint('fetchRemote: fetching ' + 10*progressCur + '% ...');
72
+ progressLast = progressCur;
73
+ }
74
+ }
75
+ }
76
+
77
+ var position = 0;
78
+ var chunksAll = new Uint8Array(receivedLength);
79
+
80
+ for (var chunk of chunks) {
81
+ chunksAll.set(chunk, position);
82
+ position += chunk.length;
83
+ }
84
+
85
+ return chunksAll;
86
+ }
87
+
88
+ // load remote data
89
+ // - check if the data is already in the IndexedDB
90
+ // - if not, fetch it from the remote URL and store it in the IndexedDB
91
+ function loadRemote(url, dst, size_mb, cbProgress, cbReady, cbCancel, cbPrint) {
92
+ // query the storage quota and print it
93
+ navigator.storage.estimate().then(function (estimate) {
94
+ cbPrint('loadRemote: storage quota: ' + estimate.quota + ' bytes');
95
+ cbPrint('loadRemote: storage usage: ' + estimate.usage + ' bytes');
96
+ });
97
+
98
+ // check if the data is already in the IndexedDB
99
+ var rq = indexedDB.open(dbName, dbVersion);
100
+
101
+ rq.onupgradeneeded = function (event) {
102
+ var db = event.target.result;
103
+ if (db.version == 1) {
104
+ var os = db.createObjectStore('models', { autoIncrement: false });
105
+ cbPrint('loadRemote: created IndexedDB ' + db.name + ' version ' + db.version);
106
+ } else {
107
+ // clear the database
108
+ var os = event.currentTarget.transaction.objectStore('models');
109
+ os.clear();
110
+ cbPrint('loadRemote: cleared IndexedDB ' + db.name + ' version ' + db.version);
111
+ }
112
+ };
113
+
114
+ rq.onsuccess = function (event) {
115
+ var db = event.target.result;
116
+ var tx = db.transaction(['models'], 'readonly');
117
+ var os = tx.objectStore('models');
118
+ var rq = os.get(url);
119
+
120
+ rq.onsuccess = function (event) {
121
+ if (rq.result) {
122
+ cbPrint('loadRemote: "' + url + '" is already in the IndexedDB');
123
+ cbReady(dst, rq.result);
124
+ } else {
125
+ // data is not in the IndexedDB
126
+ cbPrint('loadRemote: "' + url + '" is not in the IndexedDB');
127
+
128
+ // alert and ask the user to confirm
129
+ if (!confirm(
130
+ 'You are about to download ' + size_mb + ' MB of data.\n' +
131
+ 'The model data will be cached in the browser for future use.\n\n' +
132
+ 'Press OK to continue.')) {
133
+ cbCancel();
134
+ return;
135
+ }
136
+
137
+ fetchRemote(url, cbProgress, cbPrint).then(function (data) {
138
+ if (data) {
139
+ // store the data in the IndexedDB
140
+ var rq = indexedDB.open(dbName, dbVersion);
141
+ rq.onsuccess = function (event) {
142
+ var db = event.target.result;
143
+ var tx = db.transaction(['models'], 'readwrite');
144
+ var os = tx.objectStore('models');
145
+ var rq = os.put(data, url);
146
+
147
+ rq.onsuccess = function (event) {
148
+ cbPrint('loadRemote: "' + url + '" stored in the IndexedDB');
149
+ cbReady(dst, data);
150
+ };
151
+
152
+ rq.onerror = function (event) {
153
+ cbPrint('loadRemote: failed to store "' + url + '" in the IndexedDB');
154
+ cbCancel();
155
+ };
156
+ };
157
+ }
158
+ });
159
+ }
160
+ };
161
+
162
+ rq.onerror = function (event) {
163
+ cbPrint('loadRemote: failed to get data from the IndexedDB');
164
+ cbCancel();
165
+ };
166
+ };
167
+
168
+ rq.onerror = function (event) {
169
+ cbPrint('loadRemote: failed to open IndexedDB');
170
+ cbCancel();
171
+ };
172
+
173
+ rq.onblocked = function (event) {
174
+ cbPrint('loadRemote: failed to open IndexedDB: blocked');
175
+ cbCancel();
176
+ };
177
+
178
+ rq.onabort = function (event) {
179
+ cbPrint('loadRemote: failed to open IndexedDB: abort');
180
+
181
+ };
182
+ }
183
+
index.html CHANGED
@@ -1,19 +1,555 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!doctype html>
2
+ <html lang="en-us">
3
+ <head>
4
+ <title>whisper.cpp : WASM example</title>
5
+
6
+ <style>
7
+ #output {
8
+ width: 100%;
9
+ height: 100%;
10
+ margin: 0 auto;
11
+ margin-top: 10px;
12
+ border-left: 0px;
13
+ border-right: 0px;
14
+ padding-left: 0px;
15
+ padding-right: 0px;
16
+ display: block;
17
+ background-color: black;
18
+ color: white;
19
+ font-size: 10px;
20
+ font-family: 'Lucida Console', Monaco, monospace;
21
+ outline: none;
22
+ white-space: pre;
23
+ overflow-wrap: normal;
24
+ overflow-x: scroll;
25
+ }
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <div id="main-container">
30
+ <b>Minimal <a href="https://github.com/ggerganov/whisper.cpp">whisper.cpp</a> example running fully in the browser</b>
31
+
32
+ <br><br>
33
+
34
+ Usage instructions:<br>
35
+ <ul>
36
+ <li>Load a ggml model file (you can obtain one from <a href="https://ggml.ggerganov.com/">here</a>, recommended: <b>tiny</b> or <b>base</b>)</li>
37
+ <li>Select audio file to transcribe or record audio from the microphone (sample: <a href="https://whisper.ggerganov.com/jfk.wav">jfk.wav</a>)</li>
38
+ <li>Click on the "Transcribe" button to start the transcription</li>
39
+ </ul>
40
+
41
+ Note that the computation is quite heavy and may take a few seconds to complete.<br>
42
+ The transcription results will be displayed in the text area below.<br><br>
43
+ <b>Important: your browser must support WASM SIMD instructions for this to work.</b>
44
+
45
+ <br><br><hr>
46
+
47
+ <div id="model">
48
+ Whisper model: <span id="model-whisper-status"></span>
49
+ <button id="fetch-whisper-tiny-en" onclick="loadWhisper('tiny.en')">tiny.en (75 MB)</button>
50
+ <button id="fetch-whisper-tiny" onclick="loadWhisper('tiny')">tiny (75 MB)</button>
51
+ <button id="fetch-whisper-base-en" onclick="loadWhisper('base.en')">base.en (142 MB)</button>
52
+ <button id="fetch-whisper-base" onclick="loadWhisper('base')">base (142 MB)</button>
53
+ <span id="fetch-whisper-progress"></span>
54
+
55
+ <input type="file" id="whisper-file" name="file" onchange="loadFile(event, 'whisper.bin')" />
56
+ </div>
57
+
58
+ <br>
59
+
60
+ <!-- radio button to select between file upload or microphone -->
61
+ <div id="input">
62
+ Input:
63
+ <input type="radio" id="file" name="input" value="file" checked="checked" onchange="changeInput('file')" /> File
64
+ <input type="radio" id="mic" name="input" value="mic" onchange="changeInput('mic')" /> Microphone
65
+ </div>
66
+
67
+ <br>
68
+
69
+ <div id="input_file">
70
+ Audio file:
71
+ <input type="file" id="file" name="file" onchange="loadAudio(event)" />
72
+ </div>
73
+
74
+ <div id="input_mic" style="display: none;">
75
+ Microphone:
76
+ <button id="start" onclick="startRecording()">Start</button>
77
+ <button id="stop" onclick="stopRecording()" disabled>Stop</button>
78
+
79
+ <!-- progress bar to show recording progress -->
80
+ <br><br>
81
+ <div id="progress" style="display: none;">
82
+ <div id="progress-bar" style="width: 0%; height: 10px; background-color: #4CAF50;"></div>
83
+ <div id="progress-text">0%</div>
84
+ </div>
85
+ </div>
86
+
87
+ <audio controls="controls" id="audio" loop hidden>
88
+ Your browser does not support the &lt;audio&gt; tag.
89
+ <source id="source" src="" type="audio/wav" />
90
+ </audio>
91
+
92
+ <hr><br>
93
+
94
+ <table>
95
+ <tr>
96
+ <td>
97
+ Language:
98
+ <select id="language" name="language">
99
+ <option value="en">English</option>
100
+ <option value="ar">Arabic</option>
101
+ <option value="hy">Armenian</option>
102
+ <option value="az">Azerbaijani</option>
103
+ <option value="eu">Basque</option>
104
+ <option value="be">Belarusian</option>
105
+ <option value="bn">Bengali</option>
106
+ <option value="bg">Bulgarian</option>
107
+ <option value="ca">Catalan</option>
108
+ <option value="zh">Chinese</option>
109
+ <option value="hr">Croatian</option>
110
+ <option value="cs">Czech</option>
111
+ <option value="da">Danish</option>
112
+ <option value="nl">Dutch</option>
113
+ <option value="en">English</option>
114
+ <option value="et">Estonian</option>
115
+ <option value="tl">Filipino</option>
116
+ <option value="fi">Finnish</option>
117
+ <option value="fr">French</option>
118
+ <option value="gl">Galician</option>
119
+ <option value="ka">Georgian</option>
120
+ <option value="de">German</option>
121
+ <option value="el">Greek</option>
122
+ <option value="gu">Gujarati</option>
123
+ <option value="iw">Hebrew</option>
124
+ <option value="hi">Hindi</option>
125
+ <option value="hu">Hungarian</option>
126
+ <option value="is">Icelandic</option>
127
+ <option value="id">Indonesian</option>
128
+ <option value="ga">Irish</option>
129
+ <option value="it">Italian</option>
130
+ <option value="ja">Japanese</option>
131
+ <option value="kn">Kannada</option>
132
+ <option value="ko">Korean</option>
133
+ <option value="la">Latin</option>
134
+ <option value="lv">Latvian</option>
135
+ <option value="lt">Lithuanian</option>
136
+ <option value="mk">Macedonian</option>
137
+ <option value="ms">Malay</option>
138
+ <option value="mt">Maltese</option>
139
+ <option value="no">Norwegian</option>
140
+ <option value="fa">Persian</option>
141
+ <option value="pl">Polish</option>
142
+ <option value="pt">Portuguese</option>
143
+ <option value="ro">Romanian</option>
144
+ <option value="ru">Russian</option>
145
+ <option value="sr">Serbian</option>
146
+ <option value="sk">Slovak</option>
147
+ <option value="sl">Slovenian</option>
148
+ <option value="es">Spanish</option>
149
+ <option value="sw">Swahili</option>
150
+ <option value="sv">Swedish</option>
151
+ <option value="ta">Tamil</option>
152
+ <option value="te">Telugu</option>
153
+ <option value="th">Thai</option>
154
+ <option value="tr">Turkish</option>
155
+ <option value="uk">Ukrainian</option>
156
+ <option value="ur">Urdu</option>
157
+ <option value="vi">Vietnamese</option>
158
+ <option value="cy">Welsh</option>
159
+ <option value="yi">Yiddish</option>
160
+ </select>
161
+ </td>
162
+ <td>
163
+ <button onclick="onProcess(false);">Transcribe</button>
164
+ </td>
165
+ <td>
166
+ <button onclick="onProcess(true);">Translate</button>
167
+ </td>
168
+ </tr>
169
+ </table>
170
+
171
+ <br>
172
+
173
+ <!-- textarea with height filling the rest of the page -->
174
+ <textarea id="output" rows="20"></textarea>
175
+
176
+ <br><br>
177
+
178
+ <div class="cell-version">
179
+ <span>
180
+ |
181
+ Build time: <span class="nav-link">@GIT_DATE@</span> |
182
+ Commit hash: <a class="nav-link" href="https://github.com/ggerganov/whisper.cpp/commit/@GIT_SHA1@">@GIT_SHA1@</a> |
183
+ Commit subject: <span class="nav-link">@GIT_COMMIT_SUBJECT@</span> |
184
+ <a class="nav-link" href="https://github.com/ggerganov/whisper.cpp/tree/master/examples/whisper.wasm">Source Code</a> |
185
+ </span>
186
+ </div>
187
+ </div>
188
+
189
+ <script type="text/javascript" src="helpers.js"></script>
190
+ <script type='text/javascript'>
191
+ // TODO: convert audio buffer to WAV
192
+ function setAudio(audio) {
193
+ //if (audio) {
194
+ // // convert to 16-bit PCM
195
+ // var blob = new Blob([audio], { type: 'audio/wav' });
196
+ // var url = URL.createObjectURL(blob);
197
+ // document.getElementById('source').src = url;
198
+ // document.getElementById('audio').hidden = false;
199
+ // document.getElementById('audio').loop = false;
200
+ // document.getElementById('audio').load();
201
+ //} else {
202
+ // document.getElementById('audio').hidden = true;
203
+ //}
204
+ }
205
+
206
+ function changeInput(input) {
207
+ if (input == 'file') {
208
+ document.getElementById('input_file').style.display = 'block';
209
+ document.getElementById('input_mic' ).style.display = 'none';
210
+ document.getElementById('progress' ).style.display = 'none';
211
+ } else {
212
+ document.getElementById('input_file').style.display = 'none';
213
+ document.getElementById('input_mic' ).style.display = 'block';
214
+ document.getElementById('progress' ).style.display = 'block';
215
+ }
216
+ }
217
+
218
+ var Module = {
219
+ print: printTextarea,
220
+ printErr: printTextarea,
221
+ setStatus: function(text) {
222
+ printTextarea('js: ' + text);
223
+ },
224
+ monitorRunDependencies: function(left) {
225
+ }
226
+ };
227
+
228
+ // web audio context
229
+ var context = null;
230
+
231
+ // audio data
232
+ var audio = null;
233
+
234
+ // the whisper instance
235
+ var instance = null;
236
+ var model_whisper = '';
237
+
238
+ // helper function
239
+ function convertTypedArray(src, type) {
240
+ var buffer = new ArrayBuffer(src.byteLength);
241
+ var baseView = new src.constructor(buffer).set(src);
242
+ return new type(buffer);
243
+ }
244
+
245
+ //
246
+ // load model
247
+ //
248
+
249
+ let dbVersion = 1
250
+ let dbName = 'whisper.ggerganov.com';
251
+ let indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB
252
+
253
+ function storeFS(fname, buf) {
254
+ // write to WASM file using FS_createDataFile
255
+ // if the file exists, delete it
256
+ try {
257
+ Module.FS_unlink(fname);
258
+ } catch (e) {
259
+ // ignore
260
+ }
261
+
262
+ Module.FS_createDataFile("/", fname, buf, true, true);
263
+
264
+ model_whisper = fname;
265
+
266
+ document.getElementById('model-whisper-status').innerHTML = 'loaded "' + model_whisper + '"!';
267
+
268
+ printTextarea('storeFS: stored model: ' + fname + ' size: ' + buf.length);
269
+ }
270
+
271
+ function loadFile(event, fname) {
272
+ var file = event.target.files[0] || null;
273
+ if (file == null) {
274
+ return;
275
+ }
276
+
277
+ printTextarea("loadFile: loading model: " + file.name + ", size: " + file.size + " bytes");
278
+ printTextarea('loadFile: please wait ...');
279
+
280
+ var reader = new FileReader();
281
+ reader.onload = function(event) {
282
+ var buf = new Uint8Array(reader.result);
283
+ storeFS(fname, buf);
284
+ }
285
+ reader.readAsArrayBuffer(file);
286
+
287
+ document.getElementById('fetch-whisper-tiny-en').style.display = 'none';
288
+ document.getElementById('fetch-whisper-base-en').style.display = 'none';
289
+ document.getElementById('fetch-whisper-tiny' ).style.display = 'none';
290
+ document.getElementById('fetch-whisper-base' ).style.display = 'none';
291
+ document.getElementById('whisper-file' ).style.display = 'none';
292
+ document.getElementById('model-whisper-status' ).innerHTML = 'loaded model: ' + file.name;
293
+ }
294
+
295
+ function loadWhisper(model) {
296
+ let urls = {
297
+ 'tiny.en': 'https://huggingface.co/datasets/ggerganov/whisper.cpp/resolve/main/ggml-tiny.en.bin',
298
+ 'tiny': 'https://huggingface.co/datasets/ggerganov/whisper.cpp/resolve/main/ggml-model-whisper-tiny.bin',
299
+ 'base.en': 'https://huggingface.co/datasets/ggerganov/whisper.cpp/resolve/main/ggml-model-whisper-base.en.bin',
300
+ 'base': 'https://huggingface.co/datasets/ggerganov/whisper.cpp/resolve/main/ggml-model-whisper-base.bin',
301
+ };
302
+
303
+ let sizes = {
304
+ 'tiny.en': 75,
305
+ 'tiny': 75,
306
+ 'base.en': 142,
307
+ 'base': 142,
308
+ };
309
+
310
+ let url = urls[model];
311
+ let dst = 'whisper.bin';
312
+ let size_mb = sizes[model];
313
+
314
+ model_whisper = model;
315
+
316
+ document.getElementById('fetch-whisper-tiny-en').style.display = 'none';
317
+ document.getElementById('fetch-whisper-base-en').style.display = 'none';
318
+ document.getElementById('fetch-whisper-tiny' ).style.display = 'none';
319
+ document.getElementById('fetch-whisper-base' ).style.display = 'none';
320
+ document.getElementById('whisper-file' ).style.display = 'none';
321
+ document.getElementById('model-whisper-status' ).innerHTML = 'loading model: ' + model;
322
+
323
+ cbProgress = function(p) {
324
+ let el = document.getElementById('fetch-whisper-progress');
325
+ el.innerHTML = Math.round(100*p) + '%';
326
+ };
327
+
328
+ cbCancel = function() {
329
+ var el;
330
+ el = document.getElementById('fetch-whisper-tiny-en'); if (el) el.style.display = 'inline-block';
331
+ el = document.getElementById('fetch-whisper-base-en'); if (el) el.style.display = 'inline-block';
332
+ el = document.getElementById('fetch-whisper-tiny' ); if (el) el.style.display = 'inline-block';
333
+ el = document.getElementById('fetch-whisper-base' ); if (el) el.style.display = 'inline-block';
334
+ el = document.getElementById('whisper-file' ); if (el) el.style.display = 'inline-block';
335
+ el = document.getElementById('model-whisper-status' ); if (el) el.innerHTML = '';
336
+ };
337
+
338
+ loadRemote(url, dst, size_mb, cbProgress, storeFS, cbCancel, printTextarea);
339
+ }
340
+
341
+ //
342
+ // audio file
343
+ //
344
+
345
+ const kMaxAudio_s = 120;
346
+ const kSampleRate = 16000;
347
+
348
+ window.AudioContext = window.AudioContext || window.webkitAudioContext;
349
+ window.OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
350
+
351
+ function loadAudio(event) {
352
+ if (!context) {
353
+ context = new AudioContext({
354
+ sampleRate: kSampleRate,
355
+ channelCount: 1,
356
+ echoCancellation: false,
357
+ autoGainControl: true,
358
+ noiseSuppression: true,
359
+ });
360
+ }
361
+
362
+ var file = event.target.files[0] || null;
363
+ if (file == null) {
364
+ return;
365
+ }
366
+
367
+ printTextarea('js: loading audio: ' + file.name + ', size: ' + file.size + ' bytes');
368
+ printTextarea('js: please wait ...');
369
+
370
+ var reader = new FileReader();
371
+ reader.onload = function(event) {
372
+ var buf = new Uint8Array(reader.result);
373
+
374
+ context.decodeAudioData(buf.buffer, function(audioBuffer) {
375
+ var offlineContext = new OfflineAudioContext(audioBuffer.numberOfChannels, audioBuffer.length, audioBuffer.sampleRate);
376
+ var source = offlineContext.createBufferSource();
377
+ source.buffer = audioBuffer;
378
+ source.connect(offlineContext.destination);
379
+ source.start(0);
380
+
381
+ offlineContext.startRendering().then(function(renderedBuffer) {
382
+ audio = renderedBuffer.getChannelData(0);
383
+ printTextarea('js: audio loaded, size: ' + audio.length);
384
+
385
+ // truncate to first 30 seconds
386
+ if (audio.length > kMaxAudio_s*kSampleRate) {
387
+ audio = audio.slice(0, kMaxAudio_s*kSampleRate);
388
+ printTextarea('js: truncated audio to first ' + kMaxAudio_s + ' seconds');
389
+ }
390
+
391
+ setAudio(audio);
392
+ });
393
+ }, function(e) {
394
+ printTextarea('js: error decoding audio: ' + e);
395
+ audio = null;
396
+ setAudio(audio);
397
+ });
398
+ }
399
+ reader.readAsArrayBuffer(file);
400
+ }
401
+
402
+ //
403
+ // microphone
404
+ //
405
+
406
+ var mediaRecorder = null;
407
+ var doRecording = false;
408
+ var startTime = 0;
409
+
410
+ function stopRecording() {
411
+ doRecording = false;
412
+ }
413
+
414
+ // record up to kMaxAudio_s seconds of audio from the microphone
415
+ // check if doRecording is false every 1000 ms and stop recording if so
416
+ // update progress information
417
+ function startRecording() {
418
+ if (!context) {
419
+ context = new AudioContext({
420
+ sampleRate: kSampleRate,
421
+ channelCount: 1,
422
+ echoCancellation: false,
423
+ autoGainControl: true,
424
+ noiseSuppression: true,
425
+ });
426
+ }
427
+
428
+ document.getElementById('start').disabled = true;
429
+ document.getElementById('stop').disabled = false;
430
+
431
+ document.getElementById('progress-bar').style.width = '0%';
432
+ document.getElementById('progress-text').innerHTML = '0%';
433
+
434
+ doRecording = true;
435
+ startTime = Date.now();
436
+
437
+ var chunks = [];
438
+ var stream = null;
439
+
440
+ navigator.mediaDevices.getUserMedia({audio: true, video: false})
441
+ .then(function(s) {
442
+ stream = s;
443
+ mediaRecorder = new MediaRecorder(stream);
444
+ mediaRecorder.ondataavailable = function(e) {
445
+ chunks.push(e.data);
446
+ };
447
+ mediaRecorder.onstop = function(e) {
448
+ var blob = new Blob(chunks, { 'type' : 'audio/ogg; codecs=opus' });
449
+ chunks = [];
450
+
451
+ document.getElementById('start').disabled = false;
452
+ document.getElementById('stop').disabled = true;
453
+
454
+ var reader = new FileReader();
455
+ reader.onload = function(event) {
456
+ var buf = new Uint8Array(reader.result);
457
+
458
+ context.decodeAudioData(buf.buffer, function(audioBuffer) {
459
+ var offlineContext = new OfflineAudioContext(audioBuffer.numberOfChannels, audioBuffer.length, audioBuffer.sampleRate);
460
+ var source = offlineContext.createBufferSource();
461
+ source.buffer = audioBuffer;
462
+ source.connect(offlineContext.destination);
463
+ source.start(0);
464
+
465
+ offlineContext.startRendering().then(function(renderedBuffer) {
466
+ audio = renderedBuffer.getChannelData(0);
467
+ printTextarea('js: audio recorded, size: ' + audio.length);
468
+
469
+ // truncate to first 30 seconds
470
+ if (audio.length > kMaxAudio_s*kSampleRate) {
471
+ audio = audio.slice(0, kMaxAudio_s*kSampleRate);
472
+ printTextarea('js: truncated audio to first ' + kMaxAudio_s + ' seconds');
473
+ }
474
+ setAudio(audio);
475
+ });
476
+ }, function(e) {
477
+ printTextarea('js: error decoding audio: ' + e);
478
+ audio = null;
479
+ setAudio(audio);
480
+ });
481
+ }
482
+
483
+ reader.readAsArrayBuffer(blob);
484
+ };
485
+ mediaRecorder.start();
486
+ })
487
+ .catch(function(err) {
488
+ printTextarea('js: error getting audio stream: ' + err);
489
+ });
490
+
491
+ var interval = setInterval(function() {
492
+ if (!doRecording) {
493
+ clearInterval(interval);
494
+ mediaRecorder.stop();
495
+ stream.getTracks().forEach(function(track) {
496
+ track.stop();
497
+ });
498
+ }
499
+
500
+ document.getElementById('progress-bar').style.width = (100*(Date.now() - startTime)/1000/kMaxAudio_s) + '%';
501
+ document.getElementById('progress-text').innerHTML = (100*(Date.now() - startTime)/1000/kMaxAudio_s).toFixed(0) + '%';
502
+ }, 1000);
503
+
504
+ printTextarea('js: recording ...');
505
+
506
+ setTimeout(function() {
507
+ if (doRecording) {
508
+ printTextarea('js: recording stopped after ' + kMaxAudio_s + ' seconds');
509
+ stopRecording();
510
+ }
511
+ }, kMaxAudio_s*1000);
512
+ }
513
+
514
+ //
515
+ // transcribe
516
+ //
517
+
518
+ function onProcess(translate) {
519
+ if (!instance) {
520
+ instance = Module.init('whisper.bin');
521
+
522
+ if (instance) {
523
+ printTextarea("js: whisper initialized, instance: " + instance);
524
+ document.getElementById('model').innerHTML = 'Model loaded: ' + model_whisper;
525
+ }
526
+ }
527
+
528
+ if (!instance) {
529
+ printTextarea("js: failed to initialize whisper");
530
+ return;
531
+ }
532
+
533
+ if (!audio) {
534
+ printTextarea("js: no audio data");
535
+ return;
536
+ }
537
+
538
+ if (instance) {
539
+ printTextarea('');
540
+ printTextarea('js: processing - this might take a while ...');
541
+ printTextarea('');
542
+
543
+ setTimeout(function() {
544
+ var ret = Module.full_default(instance, audio, document.getElementById('language').value, translate);
545
+ console.log('js: full_default returned: ' + ret);
546
+ if (ret) {
547
+ printTextarea("js: whisper returned: " + ret);
548
+ }
549
+ }, 100);
550
+ }
551
+ }
552
+ </script>
553
+ <script type="text/javascript" src="main.js"></script>
554
+ </body>
555
  </html>
main.js ADDED
The diff for this file is too large to render. See raw diff