Load the capture worklet from a blob URL too; stop probing module imports the app no longer uses
Browse files- app.bundle.js +32 -3
- ctc-streaming.js +32 -3
- index.html +9 -24
app.bundle.js
CHANGED
|
@@ -681,6 +681,37 @@ Object.assign(__M, { CtcEngine });
|
|
| 681 |
const SAMPLE_RATE = 16000;
|
| 682 |
const INT16_SCALE = 32768; // gate thresholds are in Int16 RMS units (as tuned)
|
| 683 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 684 |
/**
|
| 685 |
* Energy speech/silence gate with an adaptive noise floor.
|
| 686 |
*
|
|
@@ -1007,9 +1038,7 @@ class StreamingCoordinator {
|
|
| 1007 |
},
|
| 1008 |
});
|
| 1009 |
this.audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
|
| 1010 |
-
|
| 1011 |
-
// for any page not sitting in the same directory as these scripts.
|
| 1012 |
-
await this.audioContext.audioWorklet.addModule(new URL('./ctc-capture-worklet.js', document.baseURI));
|
| 1013 |
const source = this.audioContext.createMediaStreamSource(this.mediaStream);
|
| 1014 |
const node = new AudioWorkletNode(this.audioContext, 'ctc-capture');
|
| 1015 |
node.port.onmessage = (e) => this.onSamples(e.data);
|
|
|
|
| 681 |
const SAMPLE_RATE = 16000;
|
| 682 |
const INT16_SCALE = 32768; // gate thresholds are in Int16 RMS units (as tuned)
|
| 683 |
|
| 684 |
+
/**
|
| 685 |
+
* URL to hand `audioWorklet.addModule()`.
|
| 686 |
+
*
|
| 687 |
+
* addModule fetches its argument as a *module*, which is the one request type a
|
| 688 |
+
* private Space's cross-site iframe fails to authenticate — measured there, the
|
| 689 |
+
* same file returns 200 to a plain fetch and intermittently fails as a module
|
| 690 |
+
* import. That would break dictation at the moment the user presses record, well
|
| 691 |
+
* after everything else had loaded cleanly. So fetch the source (plain fetches
|
| 692 |
+
* work) and hand addModule a blob: URL, which needs no network request at all.
|
| 693 |
+
* Falls back to the direct URL, which is what works everywhere else.
|
| 694 |
+
*
|
| 695 |
+
* Resolved against this module rather than the document so it survives being
|
| 696 |
+
* served from a subdirectory; build_bundle.py rewrites `document.baseURI` to
|
| 697 |
+
* `document.baseURI` for the bundled build, where the two are the same directory.
|
| 698 |
+
*/
|
| 699 |
+
let cachedWorkletUrl = null;
|
| 700 |
+
async function captureWorkletUrl() {
|
| 701 |
+
if (cachedWorkletUrl) return cachedWorkletUrl;
|
| 702 |
+
const direct = new URL('./ctc-capture-worklet.js', document.baseURI).href;
|
| 703 |
+
try {
|
| 704 |
+
const r = await fetch(direct);
|
| 705 |
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
| 706 |
+
const source = await r.text();
|
| 707 |
+
cachedWorkletUrl = URL.createObjectURL(new Blob([source], { type: 'text/javascript' }));
|
| 708 |
+
} catch (e) {
|
| 709 |
+
console.warn('could not preload the capture worklet; using its URL directly:', e);
|
| 710 |
+
cachedWorkletUrl = direct;
|
| 711 |
+
}
|
| 712 |
+
return cachedWorkletUrl;
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
/**
|
| 716 |
* Energy speech/silence gate with an adaptive noise floor.
|
| 717 |
*
|
|
|
|
| 1038 |
},
|
| 1039 |
});
|
| 1040 |
this.audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
|
| 1041 |
+
await this.audioContext.audioWorklet.addModule(await captureWorkletUrl());
|
|
|
|
|
|
|
| 1042 |
const source = this.audioContext.createMediaStreamSource(this.mediaStream);
|
| 1043 |
const node = new AudioWorkletNode(this.audioContext, 'ctc-capture');
|
| 1044 |
node.port.onmessage = (e) => this.onSamples(e.data);
|
ctc-streaming.js
CHANGED
|
@@ -23,6 +23,37 @@
|
|
| 23 |
const SAMPLE_RATE = 16000;
|
| 24 |
const INT16_SCALE = 32768; // gate thresholds are in Int16 RMS units (as tuned)
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
/**
|
| 27 |
* Energy speech/silence gate with an adaptive noise floor.
|
| 28 |
*
|
|
@@ -349,9 +380,7 @@ export class StreamingCoordinator {
|
|
| 349 |
},
|
| 350 |
});
|
| 351 |
this.audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
|
| 352 |
-
|
| 353 |
-
// for any page not sitting in the same directory as these scripts.
|
| 354 |
-
await this.audioContext.audioWorklet.addModule(new URL('./ctc-capture-worklet.js', import.meta.url));
|
| 355 |
const source = this.audioContext.createMediaStreamSource(this.mediaStream);
|
| 356 |
const node = new AudioWorkletNode(this.audioContext, 'ctc-capture');
|
| 357 |
node.port.onmessage = (e) => this.onSamples(e.data);
|
|
|
|
| 23 |
const SAMPLE_RATE = 16000;
|
| 24 |
const INT16_SCALE = 32768; // gate thresholds are in Int16 RMS units (as tuned)
|
| 25 |
|
| 26 |
+
/**
|
| 27 |
+
* URL to hand `audioWorklet.addModule()`.
|
| 28 |
+
*
|
| 29 |
+
* addModule fetches its argument as a *module*, which is the one request type a
|
| 30 |
+
* private Space's cross-site iframe fails to authenticate — measured there, the
|
| 31 |
+
* same file returns 200 to a plain fetch and intermittently fails as a module
|
| 32 |
+
* import. That would break dictation at the moment the user presses record, well
|
| 33 |
+
* after everything else had loaded cleanly. So fetch the source (plain fetches
|
| 34 |
+
* work) and hand addModule a blob: URL, which needs no network request at all.
|
| 35 |
+
* Falls back to the direct URL, which is what works everywhere else.
|
| 36 |
+
*
|
| 37 |
+
* Resolved against this module rather than the document so it survives being
|
| 38 |
+
* served from a subdirectory; build_bundle.py rewrites `import.meta.url` to
|
| 39 |
+
* `document.baseURI` for the bundled build, where the two are the same directory.
|
| 40 |
+
*/
|
| 41 |
+
let cachedWorkletUrl = null;
|
| 42 |
+
async function captureWorkletUrl() {
|
| 43 |
+
if (cachedWorkletUrl) return cachedWorkletUrl;
|
| 44 |
+
const direct = new URL('./ctc-capture-worklet.js', import.meta.url).href;
|
| 45 |
+
try {
|
| 46 |
+
const r = await fetch(direct);
|
| 47 |
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
| 48 |
+
const source = await r.text();
|
| 49 |
+
cachedWorkletUrl = URL.createObjectURL(new Blob([source], { type: 'text/javascript' }));
|
| 50 |
+
} catch (e) {
|
| 51 |
+
console.warn('could not preload the capture worklet; using its URL directly:', e);
|
| 52 |
+
cachedWorkletUrl = direct;
|
| 53 |
+
}
|
| 54 |
+
return cachedWorkletUrl;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
/**
|
| 58 |
* Energy speech/silence gate with an adaptive noise floor.
|
| 59 |
*
|
|
|
|
| 380 |
},
|
| 381 |
});
|
| 382 |
this.audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
|
| 383 |
+
await this.audioContext.audioWorklet.addModule(await captureWorkletUrl());
|
|
|
|
|
|
|
| 384 |
const source = this.audioContext.createMediaStreamSource(this.mediaStream);
|
| 385 |
const node = new AudioWorkletNode(this.audioContext, 'ctc-capture');
|
| 386 |
node.port.onmessage = (e) => this.onSamples(e.data);
|
index.html
CHANGED
|
@@ -117,15 +117,15 @@
|
|
| 117 |
*
|
| 118 |
* When this demo is served from a *private* Space, huggingface.co embeds it
|
| 119 |
* in a cross-site iframe, and browsers withhold the auth cookie from
|
| 120 |
-
* CORS-mode requests
|
| 121 |
-
*
|
| 122 |
-
*
|
| 123 |
-
*
|
| 124 |
-
*
|
| 125 |
*
|
| 126 |
-
*
|
| 127 |
-
*
|
| 128 |
-
*
|
| 129 |
*/
|
| 130 |
// Capture failures from the app's own scripts. Registered synchronously,
|
| 131 |
// before the <script> tags below, and in the capture phase so it also sees
|
|
@@ -172,28 +172,13 @@
|
|
| 172 |
const results = [
|
| 173 |
await probe('plain fetch (small asset)', () => status('./onnx-ctc/frontend_config.json')),
|
| 174 |
await probe('plain fetch (worklet file)', () => status('./ctc-capture-worklet.js')),
|
| 175 |
-
await probe('ES module import', async () => {
|
| 176 |
-
const m = await import('./ctc-tokenizer.js');
|
| 177 |
-
return m.CtcDecoder ? 'imported' : 'imported but empty';
|
| 178 |
-
}),
|
| 179 |
await probe('classic script (onnxruntime)', async () => {
|
| 180 |
if (typeof ort === 'undefined') throw new Error('global `ort` is undefined');
|
| 181 |
return `ort ${ort.env?.versions?.common ?? 'loaded'}`;
|
| 182 |
}),
|
| 183 |
await probe('LFS range (model weights)', () => ranged('./onnx-ctc/ctc_conformer_q4f16.onnx.data')),
|
| 184 |
await probe('LFS range (punctuator)', () => ranged('./punct_cap_seg_en.onnx')),
|
| 185 |
-
|
| 186 |
-
// Space showed 401s on precisely these while everything else loaded,
|
| 187 |
-
// so they are probed by name rather than by proxy.
|
| 188 |
-
await probe('import ctc-engine.js', async () => {
|
| 189 |
-
const m = await import('./ctc-engine.js');
|
| 190 |
-
return m.CtcEngine ? 'imported' : 'imported but empty';
|
| 191 |
-
}),
|
| 192 |
-
await probe('import ctc-streaming.js', async () => {
|
| 193 |
-
const m = await import('./ctc-streaming.js');
|
| 194 |
-
return m.StreamingCoordinator ? 'imported' : 'imported but empty';
|
| 195 |
-
}),
|
| 196 |
-
await probe('fetch app.js', () => status('./app.js')),
|
| 197 |
await probe('WebGPU adapter', async () => {
|
| 198 |
if (!navigator.gpu) throw new Error('navigator.gpu is undefined');
|
| 199 |
const a = await navigator.gpu.requestAdapter();
|
|
|
|
| 117 |
*
|
| 118 |
* When this demo is served from a *private* Space, huggingface.co embeds it
|
| 119 |
* in a cross-site iframe, and browsers withhold the auth cookie from
|
| 120 |
+
* CORS-mode requests there — ES module fetches. That is why the app ships as
|
| 121 |
+
* one bundled classic script (build_bundle.py) and why the ORT wasm glue and
|
| 122 |
+
* the capture worklet are loaded from blob: URLs. ES module imports are
|
| 123 |
+
* deliberately NOT probed here: nothing loads them any more, and they fail
|
| 124 |
+
* intermittently in this context, so probing them only cries wolf.
|
| 125 |
*
|
| 126 |
+
* What remains worth checking is that every asset the app really fetches is
|
| 127 |
+
* reachable, and that the app becomes usable. Silent on a healthy load;
|
| 128 |
+
* shows itself, with the status line, when something breaks.
|
| 129 |
*/
|
| 130 |
// Capture failures from the app's own scripts. Registered synchronously,
|
| 131 |
// before the <script> tags below, and in the capture phase so it also sees
|
|
|
|
| 172 |
const results = [
|
| 173 |
await probe('plain fetch (small asset)', () => status('./onnx-ctc/frontend_config.json')),
|
| 174 |
await probe('plain fetch (worklet file)', () => status('./ctc-capture-worklet.js')),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
await probe('classic script (onnxruntime)', async () => {
|
| 176 |
if (typeof ort === 'undefined') throw new Error('global `ort` is undefined');
|
| 177 |
return `ort ${ort.env?.versions?.common ?? 'loaded'}`;
|
| 178 |
}),
|
| 179 |
await probe('LFS range (model weights)', () => ranged('./onnx-ctc/ctc_conformer_q4f16.onnx.data')),
|
| 180 |
await probe('LFS range (punctuator)', () => ranged('./punct_cap_seg_en.onnx')),
|
| 181 |
+
await probe('fetch app bundle', () => status('./app.bundle.js')),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
await probe('WebGPU adapter', async () => {
|
| 183 |
if (!navigator.gpu) throw new Error('navigator.gpu is undefined');
|
| 184 |
const a = await navigator.gpu.requestAdapter();
|