@tryhamster/gerbil 1.0.0-rc.19 → 1.0.0-rc.20

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.
@@ -157,6 +157,39 @@ function resolveModel(modelId) {
157
157
  * gerbil.terminate();
158
158
  * ```
159
159
  */
160
+ /** Generates inline JS code for CDN fallback import (used in worker strings) */
161
+ const CDN_FALLBACK_CODE = `
162
+ const CDN_URLS = ${JSON.stringify([
163
+ "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1",
164
+ "https://esm.sh/@huggingface/transformers@3.8.1",
165
+ "https://unpkg.com/@huggingface/transformers@3.8.1"
166
+ ])};
167
+
168
+ let transformers = null;
169
+ let lastCdnError = null;
170
+
171
+ for (const cdnUrl of CDN_URLS) {
172
+ try {
173
+ transformers = await import(cdnUrl);
174
+ break;
175
+ } catch (err) {
176
+ lastCdnError = err;
177
+ console.warn("CDN import failed (" + cdnUrl + "):", err.message);
178
+ }
179
+ }
180
+ `;
181
+ /**
182
+ * Get a helpful error message when WebGPU is not supported.
183
+ * Detects Safari/iOS for tailored guidance.
184
+ */
185
+ function getWebGPUErrorMessage() {
186
+ if (typeof navigator === "undefined") return "WebGPU requires a browser environment.";
187
+ const ua = navigator.userAgent;
188
+ const isSafari = /Safari/.test(ua) && !/Chrome/.test(ua);
189
+ const isIOS = /iPhone|iPad|iPod/.test(ua);
190
+ if (isSafari || isIOS) return "WebGPU requires Safari 26+ (iOS 19+). Please update your device or use Chrome.";
191
+ return "WebGPU not supported. Use Chrome 113+, Edge 113+, or Safari 26+.";
192
+ }
160
193
  /**
161
194
  * Create a Gerbil worker for streaming WebGPU inference
162
195
  *
@@ -167,17 +200,19 @@ async function createGerbilWorker(options = {}) {
167
200
  const { modelId = "qwen3-0.6b", onProgress, onToken, onComplete, onError } = options;
168
201
  const source = resolveModel(modelId);
169
202
  return new Promise((resolve, reject) => {
170
- const blob = new Blob([`
171
- import {
172
- AutoTokenizer,
173
- AutoModelForCausalLM,
174
- AutoProcessor,
175
- AutoModelForImageTextToText,
176
- RawImage,
177
- TextStreamer,
178
- InterruptableStoppingCriteria,
179
- env,
180
- } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1";
203
+ const workerCode = `
204
+ // Load transformers.js with CDN fallback
205
+ ${CDN_FALLBACK_CODE}
206
+
207
+ if (!transformers) {
208
+ const errorMsg = "Failed to load ML library from all CDNs. Last error: " +
209
+ (lastCdnError?.message || "unknown") +
210
+ ". Check your network connection.";
211
+ self.postMessage({ status: "error", error: errorMsg });
212
+ throw new Error(errorMsg);
213
+ }
214
+
215
+ const { AutoTokenizer, AutoModelForCausalLM, AutoProcessor, AutoModelForImageTextToText, RawImage, TextStreamer, InterruptableStoppingCriteria, env } = transformers;
181
216
 
182
217
  // Enable IndexedDB caching for browser (prevents re-downloading models)
183
218
  env.useBrowserCache = true;
@@ -207,7 +242,24 @@ async function createGerbilWorker(options = {}) {
207
242
  modelId.toLowerCase().includes("vlm");
208
243
 
209
244
  const dtype = options.dtype || "q4f16";
210
- const device = options.device || "webgpu";
245
+ let device = options.device || "webgpu";
246
+ let usedFallback = false;
247
+
248
+ // Helper to load model with WASM fallback
249
+ async function loadWithFallback(loadFn, opts) {
250
+ try {
251
+ return await loadFn({ ...opts, device });
252
+ } catch (webgpuError) {
253
+ if (device === "webgpu") {
254
+ console.warn("WebGPU failed, falling back to WASM:", webgpuError.message);
255
+ self.postMessage({ status: "fallback", backend: "wasm", reason: webgpuError.message });
256
+ device = "wasm";
257
+ usedFallback = true;
258
+ return await loadFn({ ...opts, device: "wasm" });
259
+ }
260
+ throw webgpuError;
261
+ }
262
+ }
211
263
 
212
264
  if (this.isVision) {
213
265
  // Load vision model components
@@ -218,16 +270,17 @@ async function createGerbilWorker(options = {}) {
218
270
  });
219
271
  }
220
272
  if (!this.visionModel) {
221
- this.visionModel = await AutoModelForImageTextToText.from_pretrained(modelId, {
222
- device,
223
- progress_callback: progressCallback,
224
- });
273
+ this.visionModel = await loadWithFallback(
274
+ (opts) => AutoModelForImageTextToText.from_pretrained(modelId, opts),
275
+ { progress_callback: progressCallback }
276
+ );
225
277
  }
226
278
  return {
227
279
  processor: this.processor,
228
280
  model: this.visionModel,
229
281
  tokenizer: this.processor.tokenizer,
230
- isVision: true
282
+ isVision: true,
283
+ usedFallback
231
284
  };
232
285
  } else {
233
286
  // Load text-only model components
@@ -237,16 +290,16 @@ async function createGerbilWorker(options = {}) {
237
290
  });
238
291
  }
239
292
  if (!this.model) {
240
- this.model = await AutoModelForCausalLM.from_pretrained(modelId, {
241
- dtype,
242
- device,
243
- progress_callback: progressCallback,
244
- });
293
+ this.model = await loadWithFallback(
294
+ (opts) => AutoModelForCausalLM.from_pretrained(modelId, opts),
295
+ { dtype, progress_callback: progressCallback }
296
+ );
245
297
  }
246
298
  return {
247
299
  tokenizer: this.tokenizer,
248
300
  model: this.model,
249
- isVision: false
301
+ isVision: false,
302
+ usedFallback
250
303
  };
251
304
  }
252
305
  }
@@ -527,7 +580,8 @@ async function createGerbilWorker(options = {}) {
527
580
  });
528
581
 
529
582
  self.postMessage({ status: "init" });
530
- `], { type: "application/javascript" });
583
+ `;
584
+ const blob = new Blob([workerCode], { type: "application/javascript" });
531
585
  const workerUrl = URL.createObjectURL(blob);
532
586
  const worker = new Worker(workerUrl, { type: "module" });
533
587
  let isReady = false;
@@ -580,7 +634,14 @@ async function createGerbilWorker(options = {}) {
580
634
  }
581
635
  };
582
636
  worker.onerror = (e) => {
583
- const error = e.message || "Worker error";
637
+ let error = e.message || "";
638
+ if (!error || error === "Script error.") {
639
+ const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
640
+ const isSafari = /Safari/.test(ua) && !/Chrome/.test(ua);
641
+ const isIOS = /iPhone|iPad|iPod/.test(ua);
642
+ if (isSafari || isIOS) error = "Worker failed to initialize. Safari 26+ (iOS 19+) is required for WebGPU. Try updating your device or using Chrome.";
643
+ else error = "Worker failed to initialize. This may be due to browser restrictions or WebGPU not being available. Try Chrome 113+ or Edge 113+.";
644
+ }
584
645
  onError?.(error);
585
646
  reject(new Error(error));
586
647
  };
@@ -681,9 +742,10 @@ function useChat(options = {}) {
681
742
  useEffect(() => {
682
743
  if (!shouldLoad) return;
683
744
  if (!isWebGPUSupported()) {
684
- setError("WebGPU not supported. Use Chrome/Edge 113+.");
745
+ const gpuError = getWebGPUErrorMessage();
746
+ setError(gpuError);
685
747
  setIsLoading(false);
686
- onError?.("WebGPU not supported");
748
+ onError?.(gpuError);
687
749
  return;
688
750
  }
689
751
  mountedRef.current = true;
@@ -729,6 +791,27 @@ function useChat(options = {}) {
729
791
  workerRef.current?.terminate();
730
792
  };
731
793
  }, [model, shouldLoad]);
794
+ useEffect(() => {
795
+ if (typeof document === "undefined") return;
796
+ const handleVisibility = () => {
797
+ if (document.hidden && isGenerating && workerRef.current) {
798
+ workerRef.current.interrupt();
799
+ setIsGenerating(false);
800
+ if (currentResponse) {
801
+ setMessages((msgs) => {
802
+ if (msgs.at(-1)?.role === "assistant") return msgs.map((m, i) => i === msgs.length - 1 ? {
803
+ ...m,
804
+ content: currentResponse + " [stopped - tab hidden]"
805
+ } : m);
806
+ return msgs;
807
+ });
808
+ setCurrentResponse("");
809
+ }
810
+ }
811
+ };
812
+ document.addEventListener("visibilitychange", handleVisibility);
813
+ return () => document.removeEventListener("visibilitychange", handleVisibility);
814
+ }, [isGenerating, currentResponse]);
732
815
  useEffect(() => {
733
816
  if (!isGenerating && currentResponse) {
734
817
  setMessages((msgs) => {
@@ -928,9 +1011,10 @@ function useCompletion(options = {}) {
928
1011
  useEffect(() => {
929
1012
  if (!shouldLoad) return;
930
1013
  if (!isWebGPUSupported()) {
931
- setError("WebGPU not supported. Use Chrome/Edge 113+.");
1014
+ const gpuError = getWebGPUErrorMessage();
1015
+ setError(gpuError);
932
1016
  setIsLoading(false);
933
- onError?.("WebGPU not supported");
1017
+ onError?.(gpuError);
934
1018
  return;
935
1019
  }
936
1020
  mountedRef.current = true;
@@ -978,6 +1062,17 @@ function useCompletion(options = {}) {
978
1062
  workerRef.current?.terminate();
979
1063
  };
980
1064
  }, [model, shouldLoad]);
1065
+ useEffect(() => {
1066
+ if (typeof document === "undefined") return;
1067
+ const handleVisibility = () => {
1068
+ if (document.hidden && isGenerating && workerRef.current) {
1069
+ workerRef.current.interrupt();
1070
+ setIsGenerating(false);
1071
+ }
1072
+ };
1073
+ document.addEventListener("visibilitychange", handleVisibility);
1074
+ return () => document.removeEventListener("visibilitychange", handleVisibility);
1075
+ }, [isGenerating]);
981
1076
  const complete = useCallback((prompt, completeOptions) => {
982
1077
  return new Promise((resolve, reject) => {
983
1078
  setCompletion("");
@@ -1292,7 +1387,15 @@ const TTS_MODELS = {
1292
1387
  };
1293
1388
  const TTS_WORKER_CODE = `
1294
1389
  // TTS Worker - runs in separate thread, loads from CDN
1295
- import { pipeline, env } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1";
1390
+ ${CDN_FALLBACK_CODE}
1391
+
1392
+ if (!transformers) {
1393
+ const errorMsg = "Failed to load TTS library from all CDNs: " + (lastCdnError?.message || "unknown");
1394
+ self.postMessage({ type: "error", payload: errorMsg });
1395
+ throw new Error(errorMsg);
1396
+ }
1397
+
1398
+ const { pipeline, env } = transformers;
1296
1399
 
1297
1400
  // Configure environment
1298
1401
  env.useBrowserCache = true;
@@ -1312,13 +1415,25 @@ const TTS_WORKER_CODE = `
1312
1415
  modelType = modelId === "supertonic-66m" ? "supertonic" : "kokoro";
1313
1416
 
1314
1417
  if (modelType === "supertonic") {
1315
- // Load Supertonic using transformers.js pipeline
1316
- ttsInstance = await pipeline("text-to-speech", repo, {
1317
- device: "webgpu",
1318
- progress_callback: (progress) => {
1319
- self.postMessage({ type: "progress", payload: progress });
1320
- },
1321
- });
1418
+ // Load Supertonic using transformers.js pipeline with WASM fallback
1419
+ let device = "webgpu";
1420
+ try {
1421
+ ttsInstance = await pipeline("text-to-speech", repo, {
1422
+ device,
1423
+ progress_callback: (progress) => {
1424
+ self.postMessage({ type: "progress", payload: progress });
1425
+ },
1426
+ });
1427
+ } catch (webgpuError) {
1428
+ console.warn("WebGPU failed for TTS, falling back to WASM:", webgpuError.message);
1429
+ self.postMessage({ type: "fallback", payload: { backend: "wasm", reason: webgpuError.message } });
1430
+ ttsInstance = await pipeline("text-to-speech", repo, {
1431
+ device: "wasm",
1432
+ progress_callback: (progress) => {
1433
+ self.postMessage({ type: "progress", payload: progress });
1434
+ },
1435
+ });
1436
+ }
1322
1437
 
1323
1438
  // Load voice embeddings
1324
1439
  for (const voice of voices) {
@@ -1528,7 +1643,8 @@ function useSpeech(options = {}) {
1528
1643
  };
1529
1644
  worker.onerror = (err) => {
1530
1645
  if (!mountedRef.current) return;
1531
- const errorMsg = err.message || "Worker error";
1646
+ let errorMsg = err.message || "";
1647
+ if (!errorMsg || errorMsg === "Script error.") errorMsg = getWebGPUErrorMessage();
1532
1648
  setError(errorMsg);
1533
1649
  setIsLoading(false);
1534
1650
  setLoadingProgress({
@@ -1758,7 +1874,15 @@ function createAudioPlayer(sampleRate = 24e3) {
1758
1874
  }
1759
1875
  const STT_WORKER_CODE = `
1760
1876
  // STT Worker - runs in separate thread, loads from CDN
1761
- import { pipeline, env } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1";
1877
+ ${CDN_FALLBACK_CODE}
1878
+
1879
+ if (!transformers) {
1880
+ const errorMsg = "Failed to load STT library from all CDNs: " + (lastCdnError?.message || "unknown");
1881
+ self.postMessage({ type: "error", payload: errorMsg });
1882
+ throw new Error(errorMsg);
1883
+ }
1884
+
1885
+ const { pipeline, env } = transformers;
1762
1886
 
1763
1887
  // Configure environment
1764
1888
  env.useBrowserCache = true;
@@ -1773,13 +1897,25 @@ const STT_WORKER_CODE = `
1773
1897
  try {
1774
1898
  const { model } = payload;
1775
1899
 
1776
- // Load Whisper model
1777
- sttPipeline = await pipeline("automatic-speech-recognition", model, {
1778
- device: "webgpu",
1779
- progress_callback: (progress) => {
1780
- self.postMessage({ type: "progress", payload: progress });
1781
- },
1782
- });
1900
+ // Load Whisper model with WASM fallback
1901
+ let device = "webgpu";
1902
+ try {
1903
+ sttPipeline = await pipeline("automatic-speech-recognition", model, {
1904
+ device,
1905
+ progress_callback: (progress) => {
1906
+ self.postMessage({ type: "progress", payload: progress });
1907
+ },
1908
+ });
1909
+ } catch (webgpuError) {
1910
+ console.warn("WebGPU failed for STT, falling back to WASM:", webgpuError.message);
1911
+ self.postMessage({ type: "fallback", payload: { backend: "wasm", reason: webgpuError.message } });
1912
+ sttPipeline = await pipeline("automatic-speech-recognition", model, {
1913
+ device: "wasm",
1914
+ progress_callback: (progress) => {
1915
+ self.postMessage({ type: "progress", payload: progress });
1916
+ },
1917
+ });
1918
+ }
1783
1919
 
1784
1920
  self.postMessage({ type: "ready" });
1785
1921
  } catch (err) {
@@ -1954,7 +2090,8 @@ function useVoiceInput(options = {}) {
1954
2090
  };
1955
2091
  worker.onerror = (err) => {
1956
2092
  if (!mountedRef.current) return;
1957
- const errMsg = err.message || "Worker error";
2093
+ let errMsg = err.message || "";
2094
+ if (!errMsg || errMsg === "Script error.") errMsg = getWebGPUErrorMessage();
1958
2095
  setError(errMsg);
1959
2096
  setIsLoading(false);
1960
2097
  setLoadingProgress({
@@ -2717,7 +2854,15 @@ function useVoiceChat(options = {}) {
2717
2854
  }
2718
2855
  const EMBEDDING_WORKER_CODE = `
2719
2856
  // Embedding Worker - runs in separate thread, loads from CDN
2720
- import { pipeline, env } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1";
2857
+ ${CDN_FALLBACK_CODE}
2858
+
2859
+ if (!transformers) {
2860
+ const errorMsg = "Failed to load embedding library from all CDNs: " + (lastCdnError?.message || "unknown");
2861
+ self.postMessage({ type: "error", payload: errorMsg });
2862
+ throw new Error(errorMsg);
2863
+ }
2864
+
2865
+ const { pipeline, env } = transformers;
2721
2866
 
2722
2867
  // Configure environment
2723
2868
  env.useBrowserCache = true;
@@ -2902,7 +3047,8 @@ function useEmbedding(options = {}) {
2902
3047
  });
2903
3048
  worker.onerror = (err) => {
2904
3049
  setIsLoading(false);
2905
- const errMsg = err.message || "Worker error";
3050
+ let errMsg = err.message || "";
3051
+ if (!errMsg || errMsg === "Script error.") errMsg = getWebGPUErrorMessage();
2906
3052
  setError(errMsg);
2907
3053
  setLoadingProgress({
2908
3054
  status: "error",
@@ -3223,33 +3369,258 @@ function resolveSTTModel(modelId) {
3223
3369
  }[modelId] || modelId;
3224
3370
  }
3225
3371
  /**
3226
- * Check if WebGPU is supported
3372
+ * Check if WebGPU API exists in browser (sync check)
3373
+ * Note: This only checks if the API exists, not if it actually works.
3374
+ * Use checkWebGPUReady() for a full verification.
3227
3375
  */
3228
3376
  function isWebGPUSupported() {
3229
3377
  if (typeof navigator === "undefined") return false;
3230
3378
  return "gpu" in navigator;
3231
3379
  }
3232
3380
  /**
3381
+ * Async check if WebGPU is actually ready to use.
3382
+ * This requests an adapter to verify WebGPU truly works.
3383
+ * Returns an object with supported status and reason for failure.
3384
+ */
3385
+ async function checkWebGPUReady() {
3386
+ if (typeof navigator === "undefined") return {
3387
+ supported: false,
3388
+ reason: "Not in browser environment"
3389
+ };
3390
+ if (!("gpu" in navigator)) {
3391
+ const ua = navigator.userAgent;
3392
+ const isSafari = /Safari/.test(ua) && !/Chrome/.test(ua);
3393
+ const isIOS = /iPhone|iPad|iPod/.test(ua);
3394
+ if (isSafari || isIOS) return {
3395
+ supported: false,
3396
+ reason: "WebGPU requires Safari 26+ (iOS 19+). Update your device or use Chrome/Edge."
3397
+ };
3398
+ return {
3399
+ supported: false,
3400
+ reason: "WebGPU not available. Use Chrome 113+, Edge 113+, or Safari 26+."
3401
+ };
3402
+ }
3403
+ try {
3404
+ const adapter = await navigator.gpu.requestAdapter();
3405
+ if (!adapter) return {
3406
+ supported: false,
3407
+ reason: "No GPU adapter found. Your device may not have compatible graphics hardware."
3408
+ };
3409
+ return {
3410
+ supported: true,
3411
+ adapter: await adapter.requestAdapterInfo()
3412
+ };
3413
+ } catch (e) {
3414
+ return {
3415
+ supported: false,
3416
+ reason: e?.message || "Failed to initialize WebGPU adapter"
3417
+ };
3418
+ }
3419
+ }
3420
+ /**
3233
3421
  * Get WebGPU adapter info
3234
3422
  */
3235
3423
  async function getWebGPUInfo() {
3236
- if (!isWebGPUSupported()) return { supported: false };
3424
+ const result = await checkWebGPUReady();
3425
+ if (!result.supported) return {
3426
+ supported: false,
3427
+ reason: result.reason
3428
+ };
3429
+ return {
3430
+ supported: true,
3431
+ adapter: result.adapter?.vendor,
3432
+ device: result.adapter?.device
3433
+ };
3434
+ }
3435
+ /**
3436
+ * Minimum WebGPU buffer size required for different model sizes (in bytes)
3437
+ */
3438
+ const MODEL_BUFFER_REQUIREMENTS = {
3439
+ "smollm2-135m": 2e8,
3440
+ "smollm2-360m": 45e7,
3441
+ "qwen3-0.6b": 7e8,
3442
+ "qwen3-1.7b": 2e9,
3443
+ "whisper-tiny": 15e7,
3444
+ "kokoro-82m": 4e8
3445
+ };
3446
+ /**
3447
+ * Check if WebGPU has sufficient capabilities for a specific model.
3448
+ * Returns detailed limits and whether the model can run.
3449
+ */
3450
+ async function checkWebGPUCapabilities(modelId) {
3451
+ if (!("gpu" in navigator)) return {
3452
+ supported: false,
3453
+ reason: "WebGPU not available"
3454
+ };
3237
3455
  try {
3238
3456
  const adapter = await navigator.gpu.requestAdapter();
3239
- if (!adapter) return { supported: false };
3240
- const info = await adapter.requestAdapterInfo();
3241
- return {
3457
+ if (!adapter) return {
3458
+ supported: false,
3459
+ reason: "No GPU adapter available"
3460
+ };
3461
+ const limits = adapter.limits;
3462
+ const result = {
3242
3463
  supported: true,
3243
- adapter: info.vendor,
3244
- device: info.device
3464
+ limits: {
3465
+ maxBufferSize: limits.maxBufferSize,
3466
+ maxStorageBufferBindingSize: limits.maxStorageBufferBindingSize,
3467
+ maxComputeWorkgroupStorageSize: limits.maxComputeWorkgroupStorageSize
3468
+ },
3469
+ canRunModel: true,
3470
+ modelRequirement: void 0
3471
+ };
3472
+ if (modelId) {
3473
+ const normalizedId = modelId.toLowerCase().replace(/[^a-z0-9]/g, "-");
3474
+ const requirement = Object.entries(MODEL_BUFFER_REQUIREMENTS).find(([key]) => normalizedId.includes(key.toLowerCase().replace(/[^a-z0-9]/g, "-")))?.[1];
3475
+ if (requirement) {
3476
+ result.modelRequirement = requirement;
3477
+ result.canRunModel = limits.maxStorageBufferBindingSize >= requirement;
3478
+ if (!result.canRunModel) return {
3479
+ ...result,
3480
+ reason: `GPU buffer size (${Math.round(limits.maxStorageBufferBindingSize / 1e6)}MB) is smaller than model requirement (${Math.round(requirement / 1e6)}MB). Try a smaller model like smollm2-135m.`
3481
+ };
3482
+ }
3483
+ }
3484
+ return result;
3485
+ } catch (e) {
3486
+ return {
3487
+ supported: false,
3488
+ reason: e?.message || "Failed to check WebGPU capabilities"
3245
3489
  };
3490
+ }
3491
+ }
3492
+ /**
3493
+ * Get browser compatibility diagnostics for debugging.
3494
+ * Useful for understanding why Gerbil might not work on a device.
3495
+ */
3496
+ async function getBrowserDiagnostics() {
3497
+ const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
3498
+ const isSafari = /Safari/.test(ua) && !/Chrome/.test(ua);
3499
+ const isIOS = /iPhone|iPad|iPod/.test(ua);
3500
+ let browser = "Unknown";
3501
+ if (/Chrome\/(\d+)/.test(ua)) browser = `Chrome ${RegExp.$1}`;
3502
+ else if (/Firefox\/(\d+)/.test(ua)) browser = `Firefox ${RegExp.$1}`;
3503
+ else if (/Version\/(\d+).*Safari/.test(ua)) browser = `Safari ${RegExp.$1}`;
3504
+ else if (/Edg\/(\d+)/.test(ua)) browser = `Edge ${RegExp.$1}`;
3505
+ const webgpuResult = await checkWebGPUReady();
3506
+ const webgpu = {
3507
+ supported: webgpuResult.supported,
3508
+ reason: webgpuResult.reason,
3509
+ adapter: webgpuResult.adapter?.vendor
3510
+ };
3511
+ let moduleWorkers = false;
3512
+ try {
3513
+ const blob = new Blob(["self.postMessage('ok')"], { type: "application/javascript" });
3514
+ const url = URL.createObjectURL(blob);
3515
+ const worker = new Worker(url, { type: "module" });
3516
+ await new Promise((resolve, reject) => {
3517
+ worker.onmessage = () => resolve();
3518
+ worker.onerror = () => reject();
3519
+ setTimeout(() => reject(), 1e3);
3520
+ });
3521
+ worker.terminate();
3522
+ URL.revokeObjectURL(url);
3523
+ moduleWorkers = true;
3246
3524
  } catch {
3247
- return { supported: false };
3525
+ moduleWorkers = false;
3526
+ }
3527
+ let indexedDB = false;
3528
+ try {
3529
+ indexedDB = typeof window !== "undefined" && "indexedDB" in window;
3530
+ } catch {
3531
+ indexedDB = false;
3532
+ }
3533
+ return {
3534
+ browser,
3535
+ isSafari,
3536
+ isIOS,
3537
+ webgpu,
3538
+ moduleWorkers,
3539
+ indexedDB
3540
+ };
3541
+ }
3542
+ /**
3543
+ * Get recommended models based on device memory and capabilities.
3544
+ * Helps prevent OOM crashes on low-memory mobile devices.
3545
+ */
3546
+ function getRecommendedModels() {
3547
+ const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
3548
+ const deviceMemory = typeof navigator !== "undefined" ? navigator.deviceMemory : null;
3549
+ const isMobile = /iPhone|iPad|iPod|Android|Mobile/.test(ua);
3550
+ const availableMB = (deviceMemory ? isMobile ? deviceMemory * .4 : deviceMemory * .6 : 4) * 1024;
3551
+ let chat;
3552
+ let reason;
3553
+ if (availableMB < 500) {
3554
+ chat = "smollm2-135m";
3555
+ reason = "Very low memory device - using smallest model";
3556
+ } else if (availableMB < 1e3 || isMobile && availableMB < 1500) {
3557
+ chat = "smollm2-360m";
3558
+ reason = isMobile ? "Mobile device - using smaller model to prevent crashes" : "Low memory - using smaller model";
3559
+ } else if (availableMB < 2e3) {
3560
+ chat = "qwen3-0.6b";
3561
+ reason = "Standard model for moderate memory";
3562
+ } else {
3563
+ chat = "qwen3-1.7b";
3564
+ reason = "High memory available - using larger model for better quality";
3565
+ }
3566
+ return {
3567
+ chat,
3568
+ tts: "kokoro-82m",
3569
+ stt: "whisper-tiny.en",
3570
+ embedding: "all-MiniLM-L6-v2",
3571
+ reason,
3572
+ deviceMemory,
3573
+ isMobile
3574
+ };
3575
+ }
3576
+ /**
3577
+ * Check if there's enough storage quota for a model download.
3578
+ * Returns estimated available space and whether download should proceed.
3579
+ */
3580
+ async function checkStorageQuota(requiredMB = 500) {
3581
+ if (typeof navigator === "undefined" || !navigator.storage?.estimate) return {
3582
+ ok: true,
3583
+ availableMB: -1,
3584
+ usedMB: -1,
3585
+ quotaMB: -1,
3586
+ message: "Storage API not available"
3587
+ };
3588
+ try {
3589
+ const { quota, usage } = await navigator.storage.estimate();
3590
+ const quotaMB = Math.round((quota || 0) / 1e6);
3591
+ const usedMB = Math.round((usage || 0) / 1e6);
3592
+ const availableMB = quotaMB - usedMB;
3593
+ if (availableMB < requiredMB) return {
3594
+ ok: false,
3595
+ availableMB,
3596
+ usedMB,
3597
+ quotaMB,
3598
+ message: `Need ${requiredMB}MB but only ${availableMB}MB available. Clear browser data or free up space.`
3599
+ };
3600
+ return {
3601
+ ok: true,
3602
+ availableMB,
3603
+ usedMB,
3604
+ quotaMB
3605
+ };
3606
+ } catch (e) {
3607
+ return {
3608
+ ok: true,
3609
+ availableMB: -1,
3610
+ usedMB: -1,
3611
+ quotaMB: -1,
3612
+ message: `Storage check failed: ${e.message}`
3613
+ };
3248
3614
  }
3249
3615
  }
3250
3616
  var browser_default = {
3251
3617
  isWebGPUSupported,
3618
+ checkWebGPUReady,
3619
+ checkWebGPUCapabilities,
3252
3620
  getWebGPUInfo,
3621
+ getBrowserDiagnostics,
3622
+ getRecommendedModels,
3623
+ checkStorageQuota,
3253
3624
  createGerbilWorker,
3254
3625
  playAudio,
3255
3626
  createAudioPlayer,
@@ -3260,5 +3631,5 @@ var browser_default = {
3260
3631
  };
3261
3632
 
3262
3633
  //#endregion
3263
- export { BUILTIN_MODELS, createAudioPlayer, createGerbilWorker, browser_default as default, getWebGPUInfo, isWebGPUSupported, playAudio, preloadChatModel, preloadEmbeddingModel, preloadSTTModel, preloadTTSModel, useChat, useCompletion, useEmbedding, useSpeech, useVoiceChat, useVoiceInput };
3634
+ export { BUILTIN_MODELS, checkStorageQuota, checkWebGPUCapabilities, checkWebGPUReady, createAudioPlayer, createGerbilWorker, browser_default as default, getBrowserDiagnostics, getRecommendedModels, getWebGPUInfo, isWebGPUSupported, playAudio, preloadChatModel, preloadEmbeddingModel, preloadSTTModel, preloadTTSModel, useChat, useCompletion, useEmbedding, useSpeech, useVoiceChat, useVoiceInput };
3264
3635
  //# sourceMappingURL=index.js.map