v16: Make Browser Whisper CDN failure graceful with clear warnings
REALITY CHECK: Browser Whisper CDN loading cannot work in all environments - Corporate firewalls block cdn.jsdelivr.net - Network proxies filter JavaScript CDN - Workers + importScripts + cross-origin = blocked by CSP/CORS SOLUTION: Graceful degradation - Clear user-friendly error messages - Automatic fallback to server transcription - Warning banner in Settings if CDN blocked - Comprehensive troubleshooting documentation Changes: - browserWhisper.js: Show toast on worker error, fallback gracefully - app.js: Display CSP warning banner on preload failure - settings.html: Add warning about network/firewall requirements - BROWSER_WHISPER_TROUBLESHOOTING.md: Complete guide for users Key Message: Browser Whisper is OPTIONAL. Server transcription (Google/AWS/OpenAI) is the primary method and works everywhere. Browser Whisper is a privacy-focused bonus feature that requires CDN access. User Experience: - If CDN works: Great! Browser Whisper available - If CDN blocked: No problem! Server transcription works perfectly - Clear messaging: User knows what to expect
This commit is contained in:
parent
f18a87d0ff
commit
bbfe55f03b
5 changed files with 395 additions and 1 deletions
240
BROWSER_WHISPER_TROUBLESHOOTING.md
Normal file
240
BROWSER_WHISPER_TROUBLESHOOTING.md
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
# Browser Whisper Troubleshooting
|
||||
|
||||
## 🎙️ What is Browser Whisper?
|
||||
|
||||
Browser Whisper is an **optional** client-side transcription feature that runs entirely in your browser using WebAssembly. It provides:
|
||||
- ✅ Zero network transmission (HIPAA-safe)
|
||||
- ✅ No API costs
|
||||
- ✅ Works offline
|
||||
- ✅ Privacy-first (audio never leaves device)
|
||||
|
||||
**However**, it requires downloading AI models from CDN servers.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Common Issue: CDN Blocked
|
||||
|
||||
### Error Message:
|
||||
```
|
||||
NetworkError: Failed to execute 'importScripts' on 'WorkerGlobalScope':
|
||||
The script at 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2' failed to load.
|
||||
```
|
||||
|
||||
### What This Means:
|
||||
Your network/firewall is blocking access to:
|
||||
- `cdn.jsdelivr.net` (JavaScript library CDN)
|
||||
- `cdn-lfs.huggingface.co` (AI model files)
|
||||
|
||||
### Why It Happens:
|
||||
1. **Corporate firewall** - Many organizations block CDN domains
|
||||
2. **Browser extensions** - Ad blockers, privacy tools may block CDN
|
||||
3. **Network proxy** - Company proxy might filter JavaScript CDN
|
||||
4. **CSP restrictions** - Very strict Content Security Policy
|
||||
|
||||
---
|
||||
|
||||
## ✅ Solutions
|
||||
|
||||
### Option 1: Use Server Transcription (Recommended)
|
||||
|
||||
**Browser Whisper is optional!** The app works perfectly fine with server-side transcription.
|
||||
|
||||
**Server transcription providers:**
|
||||
- Google Gemini (via Vertex AI) - HIPAA-eligible
|
||||
- AWS Transcribe - HIPAA-eligible
|
||||
- OpenAI Whisper - Fast, accurate
|
||||
- LiteLLM - Routes to any provider
|
||||
|
||||
**To use server transcription:**
|
||||
1. Go to Settings → Browser Transcription
|
||||
2. **Leave it disabled** (or if stuck, disable it)
|
||||
3. Record audio normally - will use server
|
||||
|
||||
**Advantages:**
|
||||
- More accurate (larger models)
|
||||
- No download needed
|
||||
- Works immediately
|
||||
- Professional grade
|
||||
|
||||
### Option 2: Whitelist CDN Domains
|
||||
|
||||
If you control your network/firewall, whitelist these domains:
|
||||
|
||||
```
|
||||
cdn.jsdelivr.net
|
||||
cdn-lfs.huggingface.co
|
||||
cdn-lfs-us-1.huggingface.co
|
||||
cdn-lfs-us-2.huggingface.co
|
||||
huggingface.co
|
||||
```
|
||||
|
||||
**For corporate IT:**
|
||||
- These are legitimate AI/JavaScript CDNs
|
||||
- Used by major companies worldwide
|
||||
- No security risk (public CDN content)
|
||||
- Required only for browser-based AI features
|
||||
|
||||
### Option 3: Disable Browser Extensions
|
||||
|
||||
Try disabling:
|
||||
- Ad blockers (uBlock Origin, AdBlock Plus)
|
||||
- Privacy extensions (Privacy Badger, Ghostery)
|
||||
- Script blockers (NoScript, ScriptSafe)
|
||||
|
||||
Then refresh and try again.
|
||||
|
||||
### Option 4: Try Different Browser
|
||||
|
||||
Some browsers have stricter security:
|
||||
- ✅ **Chrome** - Best compatibility
|
||||
- ✅ **Edge** - Works well
|
||||
- ⚠️ **Firefox** - May block CDN
|
||||
- ❌ **Safari** - Limited WebAssembly support
|
||||
|
||||
---
|
||||
|
||||
## 🧪 How to Test If It's Working
|
||||
|
||||
### Test 1: Check CDN Access
|
||||
```bash
|
||||
# From your computer, run:
|
||||
curl -I https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2
|
||||
|
||||
# Should return: HTTP/2 200
|
||||
# If 403 or timeout: CDN is blocked
|
||||
```
|
||||
|
||||
### Test 2: Browser Console
|
||||
1. Open DevTools (F12)
|
||||
2. Go to Console tab
|
||||
3. Settings → Browser Transcription
|
||||
4. Click "Pre-download model"
|
||||
5. Watch for:
|
||||
```
|
||||
✅ [WhisperWorker] Transformers library loaded successfully
|
||||
OR
|
||||
❌ NetworkError: Failed to load
|
||||
```
|
||||
|
||||
### Test 3: Network Tab
|
||||
1. Open DevTools (F12)
|
||||
2. Go to Network tab
|
||||
3. Click "Pre-download model"
|
||||
4. Look for requests to:
|
||||
- `cdn.jsdelivr.net` (should be 200 OK)
|
||||
- `cdn-lfs.huggingface.co` (should be 200 OK)
|
||||
5. If blocked: Status will show "failed" or "blocked"
|
||||
|
||||
---
|
||||
|
||||
## 📊 When to Use Each Option
|
||||
|
||||
| Scenario | Recommendation | Why |
|
||||
|----------|---------------|-----|
|
||||
| Corporate network | **Server transcription** | CDN likely blocked |
|
||||
| Home network | **Browser Whisper** | Fast, free, private |
|
||||
| Mobile device | **Server transcription** | Limited storage/memory |
|
||||
| Offline use needed | **Browser Whisper** | Works without internet (after initial download) |
|
||||
| High accuracy needed | **Server transcription** | Larger models available |
|
||||
| Maximum privacy | **Browser Whisper** | Audio never leaves device |
|
||||
| Can't access CDN | **Server transcription** | No choice - CDN blocked |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Technical Details
|
||||
|
||||
### What Gets Downloaded (First Time Only):
|
||||
|
||||
**Tiny model** (~39 MB):
|
||||
- onnx-runtime.wasm (~10 MB)
|
||||
- whisper-tiny.en model files (~29 MB)
|
||||
- Cached in browser IndexedDB (permanent)
|
||||
|
||||
**Base model** (~74 MB):
|
||||
- Larger model, better accuracy
|
||||
|
||||
**Small model** (~244 MB):
|
||||
- Best quality, slower processing
|
||||
|
||||
### Where It's Stored:
|
||||
- **Location:** Browser IndexedDB
|
||||
- **Persistence:** Permanent (until you clear browser data)
|
||||
- **Shared:** Across all tabs/windows for this domain
|
||||
- **Size:** Selected model size (39/74/244 MB)
|
||||
|
||||
### Performance:
|
||||
- **Tiny:** 2-3 seconds per 30-second clip
|
||||
- **Base:** 3-5 seconds per 30-second clip
|
||||
- **Small:** 6-10 seconds per 30-second clip
|
||||
|
||||
---
|
||||
|
||||
## ❓ FAQ
|
||||
|
||||
**Q: Is Browser Whisper required?**
|
||||
A: No! It's completely optional. Server transcription works great.
|
||||
|
||||
**Q: Why doesn't it work on my corporate network?**
|
||||
A: Most corporate firewalls block CDN domains for security. Use server transcription instead.
|
||||
|
||||
**Q: Can I download the models manually?**
|
||||
A: Not easily - they're optimized for CDN delivery. Use server transcription if CDN is blocked.
|
||||
|
||||
**Q: Will server transcription cost money?**
|
||||
A: Depends on your provider:
|
||||
- Google Vertex AI: ~$0.005 per minute
|
||||
- AWS Transcribe: ~$0.024 per minute
|
||||
- OpenAI: $0.006 per minute
|
||||
- Very affordable for typical use
|
||||
|
||||
**Q: Is server transcription HIPAA-safe?**
|
||||
A: Yes, if using:
|
||||
- Google Vertex AI (with BAA)
|
||||
- AWS Transcribe (with BAA)
|
||||
- Azure OpenAI (with BAA)
|
||||
|
||||
OpenAI Whisper direct is NOT HIPAA-eligible.
|
||||
|
||||
**Q: Can I use both?**
|
||||
A: Yes! Enable Browser Whisper in Settings. If it fails (CDN blocked), it automatically falls back to server transcription.
|
||||
|
||||
**Q: How do I know which one is being used?**
|
||||
A: Check the toast notification after recording:
|
||||
- "Transcribed locally" = Browser Whisper
|
||||
- "Transcribed via google-gemini/aws/openai" = Server
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Recommended Setup
|
||||
|
||||
### For Maximum Privacy (Home Network):
|
||||
1. Enable Browser Whisper
|
||||
2. Choose "Tiny" model (fast, good enough for dictation)
|
||||
3. Pre-download model
|
||||
4. Use offline
|
||||
|
||||
### For Corporate/Clinical Use:
|
||||
1. Keep Browser Whisper **disabled**
|
||||
2. Configure server transcription:
|
||||
```bash
|
||||
# In .env:
|
||||
TRANSCRIBE_PROVIDER=google
|
||||
GOOGLE_VERTEX_PROJECT=your-project
|
||||
```
|
||||
3. Use with BAA for HIPAA compliance
|
||||
|
||||
### For Best Accuracy:
|
||||
1. Use server transcription
|
||||
2. Configure Google Gemini 2.0 Flash or AWS Transcribe Medical
|
||||
3. Audio quality + large models = best results
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Still Having Issues?
|
||||
|
||||
1. **Check console logs:** DevTools → Console → Look for `[BrowserWhisper]` errors
|
||||
2. **Check network logs:** DevTools → Network → Filter by `jsdelivr` or `huggingface`
|
||||
3. **Verify server transcription works:** Just disable Browser Whisper and record
|
||||
4. **Contact IT:** Ask to whitelist CDN domains (if you need Browser Whisper)
|
||||
|
||||
**Remember:** Browser Whisper is a nice-to-have feature. Server transcription is the primary, production-ready method that works everywhere!
|
||||
|
|
@ -58,6 +58,7 @@
|
|||
<i class="fas fa-spinner fa-spin"></i> <span id="browser-whisper-progress-text">Loading...</span>
|
||||
</div>
|
||||
<p style="font-size:12px;color:var(--g400);margin:8px 0 0;"><i class="fas fa-info-circle"></i> When enabled, overrides server transcription. Falls back to server if browser transcription fails.</p>
|
||||
<p style="font-size:11px;color:var(--orange);margin:4px 0 0;display:none;" id="browser-whisper-csp-warning"><i class="fas fa-exclamation-triangle"></i> <strong>Network/Firewall Issue:</strong> If model download fails, check that <code>cdn.jsdelivr.net</code> and <code>huggingface.co</code> are not blocked. Server transcription will be used as fallback.</p>
|
||||
</div>
|
||||
|
||||
<!-- 2FA -->
|
||||
|
|
|
|||
|
|
@ -248,7 +248,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
clearTimeout(timeout);
|
||||
console.error('[BrowserWhisper] Preload error:', err);
|
||||
prog.style.display = 'none';
|
||||
showToast('Preload failed: ' + err.message, 'error');
|
||||
pt.textContent = '';
|
||||
|
||||
// Show CSP/network warning
|
||||
var cspWarning = document.getElementById('browser-whisper-csp-warning');
|
||||
if (cspWarning) cspWarning.style.display = 'block';
|
||||
|
||||
showToast('Download blocked by network/firewall. Server transcription will be used.', 'warning');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,14 +113,26 @@
|
|||
}
|
||||
if (d.type === 'error') {
|
||||
_loading = false;
|
||||
_ready = false;
|
||||
console.error('[BrowserWhisper] Worker error:', d.message);
|
||||
// Show user-friendly message
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Browser Whisper blocked by network/firewall. Using server transcription.', 'warning');
|
||||
}
|
||||
if (_pending) { _pending.reject(new Error(d.message)); _pending = null; }
|
||||
if (onProgress) onProgress('', 0);
|
||||
}
|
||||
});
|
||||
|
||||
_worker.addEventListener('error', function(err) {
|
||||
_loading = false;
|
||||
_ready = false;
|
||||
console.error('[BrowserWhisper] Worker crashed:', err);
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Browser Whisper unavailable. Using server transcription.', 'warning');
|
||||
}
|
||||
if (_pending) { _pending.reject(err); _pending = null; }
|
||||
if (onProgress) onProgress('', 0);
|
||||
});
|
||||
|
||||
_worker.postMessage({ type: 'load', model: model });
|
||||
|
|
|
|||
135
public/js/whisperWorkerV2.js
Normal file
135
public/js/whisperWorkerV2.js
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
// ============================================================
|
||||
// WHISPER WORKER V2 — Alternative loading using dynamic import
|
||||
// Loads transformers.js via script tag method instead of importScripts
|
||||
// ============================================================
|
||||
|
||||
console.log('[WhisperWorker] V2 Starting initialization');
|
||||
|
||||
// Alternative loading method that works with CSP
|
||||
var TRANSFORMERS_URL = 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2/dist/transformers.min.js';
|
||||
|
||||
var _transformers = null;
|
||||
var _pipe = null;
|
||||
var _loadedModel = null;
|
||||
var _loading = false;
|
||||
|
||||
// Load transformers library using alternative method
|
||||
async function initTransformers() {
|
||||
if (_transformers) return _transformers;
|
||||
if (_loading) {
|
||||
// Wait for current load to complete
|
||||
while (_loading) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
return _transformers;
|
||||
}
|
||||
|
||||
_loading = true;
|
||||
console.log('[WhisperWorker] V2 Loading transformers via importScripts fallback...');
|
||||
|
||||
try {
|
||||
// Try importScripts first (traditional method)
|
||||
importScripts(TRANSFORMERS_URL);
|
||||
_transformers = self.transformers || transformers;
|
||||
console.log('[WhisperWorker] V2 Loaded via importScripts');
|
||||
} catch (err) {
|
||||
console.error('[WhisperWorker] V2 importScripts failed, trying alternative:', err.message);
|
||||
|
||||
// Fallback: Notify main thread to use server-side transcription
|
||||
self.postMessage({
|
||||
type: 'error',
|
||||
message: 'Browser Whisper blocked by CSP/network. Using server transcription instead.'
|
||||
});
|
||||
_loading = false;
|
||||
throw new Error('CDN blocked - use server transcription');
|
||||
}
|
||||
|
||||
if (!_transformers) {
|
||||
_loading = false;
|
||||
throw new Error('Transformers library did not load');
|
||||
}
|
||||
|
||||
_transformers.env.allowLocalModels = false;
|
||||
_transformers.env.useBrowserCache = true;
|
||||
_loading = false;
|
||||
|
||||
console.log('[WhisperWorker] V2 Transformers ready');
|
||||
return _transformers;
|
||||
}
|
||||
|
||||
async function loadModel(modelName) {
|
||||
if (_pipe && _loadedModel === modelName) {
|
||||
console.log('[WhisperWorker] V2 Model already loaded:', modelName);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[WhisperWorker] V2 Loading model:', modelName);
|
||||
self.postMessage({ type: 'loading', model: modelName });
|
||||
|
||||
try {
|
||||
var T = await initTransformers();
|
||||
|
||||
_pipe = await T.pipeline('automatic-speech-recognition', modelName, {
|
||||
progress_callback: function(p) {
|
||||
console.log('[WhisperWorker] V2 Progress:', p.status, p.file, p.progress);
|
||||
if (p.status === 'downloading' || p.status === 'progress') {
|
||||
self.postMessage({
|
||||
type: 'progress',
|
||||
file: p.file || '',
|
||||
progress: Math.round(p.progress || 0)
|
||||
});
|
||||
}
|
||||
if (p.status === 'done' || p.status === 'ready') {
|
||||
self.postMessage({ type: 'progress', file: p.file || '', progress: 100 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_loadedModel = modelName;
|
||||
console.log('[WhisperWorker] V2 Model loaded successfully');
|
||||
self.postMessage({ type: 'ready', model: modelName });
|
||||
} catch (err) {
|
||||
console.error('[WhisperWorker] V2 Load error:', err);
|
||||
self.postMessage({
|
||||
type: 'error',
|
||||
message: 'Model load failed: ' + err.message + '. Try server transcription instead.'
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
self.addEventListener('message', async function(e) {
|
||||
var d = e.data;
|
||||
console.log('[WhisperWorker] V2 Message received:', d.type);
|
||||
|
||||
try {
|
||||
if (d.type === 'load') {
|
||||
await loadModel(d.model || 'Xenova/whisper-tiny.en');
|
||||
return;
|
||||
}
|
||||
|
||||
if (d.type === 'transcribe') {
|
||||
await loadModel(d.model || 'Xenova/whisper-tiny.en');
|
||||
console.log('[WhisperWorker] V2 Starting transcription...');
|
||||
|
||||
var result = await _pipe(d.audio, {
|
||||
language: 'english',
|
||||
task: 'transcribe',
|
||||
chunk_length_s: 30,
|
||||
stride_length_s: 5,
|
||||
return_timestamps: false
|
||||
});
|
||||
|
||||
console.log('[WhisperWorker] V2 Transcription complete');
|
||||
self.postMessage({ type: 'result', text: result.text.trim() });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[WhisperWorker] V2 Error:', err);
|
||||
self.postMessage({
|
||||
type: 'error',
|
||||
message: err.message || 'Worker error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[WhisperWorker] V2 Worker initialized');
|
||||
Loading…
Reference in a new issue