@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.
- package/dist/browser/index.d.ts +79 -2
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +429 -58
- package/dist/browser/index.js.map +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/cli.mjs.map +1 -1
- package/dist/frameworks/react.d.mts.map +1 -1
- package/dist/skills/index.d.mts +1 -1
- package/dist/skills/index.d.mts.map +1 -1
- package/package.json +1 -1
package/dist/browser/index.js
CHANGED
|
@@ -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
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
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
|
-
|
|
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
|
|
222
|
-
|
|
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
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
745
|
+
const gpuError = getWebGPUErrorMessage();
|
|
746
|
+
setError(gpuError);
|
|
685
747
|
setIsLoading(false);
|
|
686
|
-
onError?.(
|
|
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
|
-
|
|
1014
|
+
const gpuError = getWebGPUErrorMessage();
|
|
1015
|
+
setError(gpuError);
|
|
932
1016
|
setIsLoading(false);
|
|
933
|
-
onError?.(
|
|
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
|
-
|
|
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
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
3240
|
-
|
|
3241
|
-
|
|
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
|
-
|
|
3244
|
-
|
|
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
|
-
|
|
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
|