@tryhamster/gerbil 1.0.0-rc.23 → 1.0.0-rc.24
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 +146 -2
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +472 -21
- package/dist/browser/index.js.map +1 -1
- package/dist/cli.mjs +7 -7
- package/dist/cli.mjs.map +1 -1
- package/dist/frameworks/express.d.mts +1 -3
- package/dist/frameworks/express.d.mts.map +1 -1
- package/dist/frameworks/express.mjs +3 -3
- package/dist/frameworks/express.mjs.map +1 -1
- package/dist/frameworks/fastify.d.mts +1 -1
- package/dist/frameworks/fastify.mjs +1 -1
- package/dist/frameworks/hono.d.mts +1 -1
- package/dist/frameworks/hono.mjs +1 -1
- package/dist/frameworks/next.d.mts +2 -2
- package/dist/frameworks/next.mjs +1 -1
- package/dist/frameworks/react.d.mts +1 -1
- package/dist/frameworks/react.d.mts.map +1 -1
- package/dist/frameworks/trpc.d.mts +1 -1
- package/dist/frameworks/trpc.mjs +1 -1
- package/dist/{gerbil-DJygY0sJ.d.mts → gerbil-CbnV_cG5.d.mts} +9 -2
- package/dist/gerbil-CbnV_cG5.d.mts.map +1 -0
- package/dist/{gerbil-PzPtcdeM.mjs → gerbil-DODVGr-u.mjs} +1 -1
- package/dist/{gerbil-DzZ-L6n8.mjs → gerbil-jO9anIh_.mjs} +90 -3
- package/dist/gerbil-jO9anIh_.mjs.map +1 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/integrations/ai-sdk.d.mts +1 -1
- package/dist/integrations/ai-sdk.mjs +1 -1
- package/dist/integrations/langchain.d.mts +1 -1
- package/dist/integrations/langchain.mjs +1 -1
- package/dist/integrations/llamaindex.d.mts +1 -1
- package/dist/integrations/llamaindex.mjs +1 -1
- package/dist/integrations/mcp.d.mts +2 -2
- package/dist/integrations/mcp.mjs +4 -4
- package/dist/{mcp-D161vL_C.mjs → mcp-tavZtFY1.mjs} +3 -3
- package/dist/{mcp-D161vL_C.mjs.map → mcp-tavZtFY1.mjs.map} +1 -1
- package/dist/{one-liner-C-pRqDK2.mjs → one-liner-Ba58M_6j.mjs} +2 -2
- package/dist/{one-liner-C-pRqDK2.mjs.map → one-liner-Ba58M_6j.mjs.map} +1 -1
- package/dist/{repl-D9x3TnQc.mjs → repl-BGly-o_e.mjs} +3 -3
- package/dist/skills/index.d.mts +3 -3
- package/dist/skills/index.d.mts.map +1 -1
- package/dist/skills/index.mjs +3 -3
- package/dist/{skills-D14RwyUN.mjs → skills-BKxP2pex.mjs} +2 -2
- package/dist/{skills-D14RwyUN.mjs.map → skills-BKxP2pex.mjs.map} +1 -1
- package/dist/{types-evP8RShr.d.mts → types-6uG8lC7u.d.mts} +65 -2
- package/dist/types-6uG8lC7u.d.mts.map +1 -0
- package/docs/architecture/overview.md +2 -0
- package/docs/observability.md +230 -0
- package/package.json +5 -4
- package/dist/gerbil-DJygY0sJ.d.mts.map +0 -1
- package/dist/gerbil-DzZ-L6n8.mjs.map +0 -1
- package/dist/types-evP8RShr.d.mts.map +0 -1
package/dist/browser/index.js
CHANGED
|
@@ -766,6 +766,13 @@ function useChat(options = {}) {
|
|
|
766
766
|
setIsLoading(true);
|
|
767
767
|
setShouldLoad(true);
|
|
768
768
|
}, [isLoading]);
|
|
769
|
+
useEffect(() => {
|
|
770
|
+
const crash = detectMemoryCrash();
|
|
771
|
+
if (crash.crashed) {
|
|
772
|
+
setError(crash.recommendation || "Previous model load failed due to device memory limits.");
|
|
773
|
+
onError?.(crash.recommendation || "Previous model load failed");
|
|
774
|
+
}
|
|
775
|
+
}, []);
|
|
769
776
|
useEffect(() => {
|
|
770
777
|
if (!shouldLoad) return;
|
|
771
778
|
if (!isWebGPUSupported()) {
|
|
@@ -775,13 +782,27 @@ function useChat(options = {}) {
|
|
|
775
782
|
onError?.(gpuError);
|
|
776
783
|
return;
|
|
777
784
|
}
|
|
785
|
+
const safetyCheck = isModelSafeForDevice(model);
|
|
786
|
+
if (!safetyCheck.safe) {
|
|
787
|
+
setError(safetyCheck.reason);
|
|
788
|
+
setIsLoading(false);
|
|
789
|
+
onError?.(safetyCheck.reason);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
778
792
|
mountedRef.current = true;
|
|
793
|
+
setDownloadPhase("downloading", model);
|
|
779
794
|
createGerbilWorker({
|
|
780
795
|
modelId: model,
|
|
781
796
|
onProgress: (p) => {
|
|
782
797
|
if (!mountedRef.current) return;
|
|
783
798
|
setLoadingProgress(p);
|
|
799
|
+
if (p.status === "downloading" && p.progress !== void 0) setDownloadPhase("downloading", model, {
|
|
800
|
+
bytesDownloaded: p.progress,
|
|
801
|
+
totalBytes: 100
|
|
802
|
+
});
|
|
803
|
+
else if (p.status === "loading") setDownloadPhase("initializing", model);
|
|
784
804
|
if (p.status === "ready") {
|
|
805
|
+
clearDownloadPhase();
|
|
785
806
|
setIsLoading(false);
|
|
786
807
|
setIsReady(true);
|
|
787
808
|
onReady?.();
|
|
@@ -799,6 +820,7 @@ function useChat(options = {}) {
|
|
|
799
820
|
},
|
|
800
821
|
onError: (err) => {
|
|
801
822
|
if (!mountedRef.current) return;
|
|
823
|
+
setDownloadPhase("error", model);
|
|
802
824
|
setError(err);
|
|
803
825
|
setIsGenerating(false);
|
|
804
826
|
onError?.(err);
|
|
@@ -808,6 +830,7 @@ function useChat(options = {}) {
|
|
|
808
830
|
else worker.terminate();
|
|
809
831
|
}).catch((err) => {
|
|
810
832
|
if (mountedRef.current) {
|
|
833
|
+
setDownloadPhase("error", model);
|
|
811
834
|
setError(err.message);
|
|
812
835
|
setIsLoading(false);
|
|
813
836
|
onError?.(err.message);
|
|
@@ -1035,6 +1058,13 @@ function useCompletion(options = {}) {
|
|
|
1035
1058
|
setIsLoading(true);
|
|
1036
1059
|
setShouldLoad(true);
|
|
1037
1060
|
}, [isLoading]);
|
|
1061
|
+
useEffect(() => {
|
|
1062
|
+
const crash = detectMemoryCrash();
|
|
1063
|
+
if (crash.crashed) {
|
|
1064
|
+
setError(crash.recommendation || "Previous model load failed due to device memory limits.");
|
|
1065
|
+
onError?.(crash.recommendation || "Previous model load failed");
|
|
1066
|
+
}
|
|
1067
|
+
}, []);
|
|
1038
1068
|
useEffect(() => {
|
|
1039
1069
|
if (!shouldLoad) return;
|
|
1040
1070
|
if (!isWebGPUSupported()) {
|
|
@@ -1044,13 +1074,27 @@ function useCompletion(options = {}) {
|
|
|
1044
1074
|
onError?.(gpuError);
|
|
1045
1075
|
return;
|
|
1046
1076
|
}
|
|
1077
|
+
const safetyCheck = isModelSafeForDevice(model);
|
|
1078
|
+
if (!safetyCheck.safe) {
|
|
1079
|
+
setError(safetyCheck.reason);
|
|
1080
|
+
setIsLoading(false);
|
|
1081
|
+
onError?.(safetyCheck.reason);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1047
1084
|
mountedRef.current = true;
|
|
1085
|
+
setDownloadPhase("downloading", model);
|
|
1048
1086
|
createGerbilWorker({
|
|
1049
1087
|
modelId: model,
|
|
1050
1088
|
onProgress: (p) => {
|
|
1051
1089
|
if (!mountedRef.current) return;
|
|
1052
1090
|
setLoadingProgress(p);
|
|
1091
|
+
if (p.status === "downloading" && p.progress !== void 0) setDownloadPhase("downloading", model, {
|
|
1092
|
+
bytesDownloaded: p.progress,
|
|
1093
|
+
totalBytes: 100
|
|
1094
|
+
});
|
|
1095
|
+
else if (p.status === "loading") setDownloadPhase("initializing", model);
|
|
1053
1096
|
if (p.status === "ready") {
|
|
1097
|
+
clearDownloadPhase();
|
|
1054
1098
|
setIsLoading(false);
|
|
1055
1099
|
setIsReady(true);
|
|
1056
1100
|
onReady?.();
|
|
@@ -1070,6 +1114,7 @@ function useCompletion(options = {}) {
|
|
|
1070
1114
|
},
|
|
1071
1115
|
onError: (err) => {
|
|
1072
1116
|
if (!mountedRef.current) return;
|
|
1117
|
+
setDownloadPhase("error", model);
|
|
1073
1118
|
setError(err);
|
|
1074
1119
|
setIsGenerating(false);
|
|
1075
1120
|
onError?.(err);
|
|
@@ -1079,6 +1124,7 @@ function useCompletion(options = {}) {
|
|
|
1079
1124
|
else worker.terminate();
|
|
1080
1125
|
}).catch((err) => {
|
|
1081
1126
|
if (mountedRef.current) {
|
|
1127
|
+
setDownloadPhase("error", model);
|
|
1082
1128
|
setError(err.message);
|
|
1083
1129
|
setIsLoading(false);
|
|
1084
1130
|
onError?.(err.message);
|
|
@@ -1445,12 +1491,12 @@ const TTS_WORKER_CODE = `
|
|
|
1445
1491
|
// Load Supertonic using transformers.js pipeline with WASM fallback
|
|
1446
1492
|
let device = "webgpu";
|
|
1447
1493
|
try {
|
|
1448
|
-
|
|
1494
|
+
ttsInstance = await pipeline("text-to-speech", repo, {
|
|
1449
1495
|
device,
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1496
|
+
progress_callback: (progress) => {
|
|
1497
|
+
self.postMessage({ type: "progress", payload: progress });
|
|
1498
|
+
},
|
|
1499
|
+
});
|
|
1454
1500
|
} catch (webgpuError) {
|
|
1455
1501
|
console.warn("WebGPU failed for TTS, falling back to WASM:", webgpuError.message);
|
|
1456
1502
|
self.postMessage({ type: "fallback", payload: { backend: "wasm", reason: webgpuError.message } });
|
|
@@ -1493,8 +1539,8 @@ const TTS_WORKER_CODE = `
|
|
|
1493
1539
|
|
|
1494
1540
|
// Try WebGPU first, fallback to WASM
|
|
1495
1541
|
try {
|
|
1496
|
-
|
|
1497
|
-
|
|
1542
|
+
kokoroTTS = await KokoroTTS.from_pretrained(repo, {
|
|
1543
|
+
dtype: "fp32",
|
|
1498
1544
|
device: "webgpu",
|
|
1499
1545
|
progress_callback: (progress) => {
|
|
1500
1546
|
self.postMessage({ type: "progress", payload: progress });
|
|
@@ -1506,10 +1552,10 @@ const TTS_WORKER_CODE = `
|
|
|
1506
1552
|
kokoroTTS = await KokoroTTS.from_pretrained(repo, {
|
|
1507
1553
|
dtype: "fp32",
|
|
1508
1554
|
device: "wasm",
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1555
|
+
progress_callback: (progress) => {
|
|
1556
|
+
self.postMessage({ type: "progress", payload: progress });
|
|
1557
|
+
},
|
|
1558
|
+
});
|
|
1513
1559
|
}
|
|
1514
1560
|
}
|
|
1515
1561
|
|
|
@@ -1629,10 +1675,18 @@ function useSpeech(options = {}) {
|
|
|
1629
1675
|
setIsLoading(true);
|
|
1630
1676
|
setShouldLoad(true);
|
|
1631
1677
|
}, [isLoading]);
|
|
1678
|
+
useEffect(() => {
|
|
1679
|
+
const crash = detectMemoryCrash();
|
|
1680
|
+
if (crash.crashed) {
|
|
1681
|
+
setError(crash.recommendation || "Previous model load failed due to device memory limits.");
|
|
1682
|
+
onError?.(crash.recommendation || "Previous model load failed");
|
|
1683
|
+
}
|
|
1684
|
+
}, []);
|
|
1632
1685
|
useEffect(() => {
|
|
1633
1686
|
if (!shouldLoad) return;
|
|
1634
1687
|
mountedRef.current = true;
|
|
1635
1688
|
modelIdRef.current = modelId;
|
|
1689
|
+
setDownloadPhase("downloading", modelId);
|
|
1636
1690
|
const config = TTS_MODELS[modelId];
|
|
1637
1691
|
setLoadingProgress({
|
|
1638
1692
|
status: "loading",
|
|
@@ -1649,6 +1703,7 @@ function useSpeech(options = {}) {
|
|
|
1649
1703
|
progress: Math.round(payload.progress || 0)
|
|
1650
1704
|
});
|
|
1651
1705
|
if (type === "ready") {
|
|
1706
|
+
clearDownloadPhase();
|
|
1652
1707
|
setIsLoading(false);
|
|
1653
1708
|
setIsReady(true);
|
|
1654
1709
|
setLoadingProgress({ status: "ready" });
|
|
@@ -1671,6 +1726,7 @@ function useSpeech(options = {}) {
|
|
|
1671
1726
|
playAudioData(audio, sampleRate);
|
|
1672
1727
|
}
|
|
1673
1728
|
if (type === "error") {
|
|
1729
|
+
setDownloadPhase("error", modelId);
|
|
1674
1730
|
const errorMsg = payload;
|
|
1675
1731
|
setError(errorMsg);
|
|
1676
1732
|
setIsLoading(false);
|
|
@@ -1684,6 +1740,7 @@ function useSpeech(options = {}) {
|
|
|
1684
1740
|
};
|
|
1685
1741
|
worker.onerror = (err) => {
|
|
1686
1742
|
if (!mountedRef.current) return;
|
|
1743
|
+
setDownloadPhase("error", modelId);
|
|
1687
1744
|
let errorMsg = err.message || "";
|
|
1688
1745
|
if (!errorMsg || errorMsg === "Script error.") errorMsg = getWebGPUErrorMessage();
|
|
1689
1746
|
setError(errorMsg);
|
|
@@ -1941,12 +1998,12 @@ const STT_WORKER_CODE = `
|
|
|
1941
1998
|
// Load Whisper model with WASM fallback
|
|
1942
1999
|
let device = "webgpu";
|
|
1943
2000
|
try {
|
|
1944
|
-
|
|
2001
|
+
sttPipeline = await pipeline("automatic-speech-recognition", model, {
|
|
1945
2002
|
device,
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2003
|
+
progress_callback: (progress) => {
|
|
2004
|
+
self.postMessage({ type: "progress", payload: progress });
|
|
2005
|
+
},
|
|
2006
|
+
});
|
|
1950
2007
|
} catch (webgpuError) {
|
|
1951
2008
|
console.warn("WebGPU failed for STT, falling back to WASM:", webgpuError.message);
|
|
1952
2009
|
self.postMessage({ type: "fallback", payload: { backend: "wasm", reason: webgpuError.message } });
|
|
@@ -2065,9 +2122,17 @@ function useVoiceInput(options = {}) {
|
|
|
2065
2122
|
"whisper-small.en": "onnx-community/whisper-small.en"
|
|
2066
2123
|
}[modelId] || modelId;
|
|
2067
2124
|
};
|
|
2125
|
+
useEffect(() => {
|
|
2126
|
+
const crash = detectMemoryCrash();
|
|
2127
|
+
if (crash.crashed) {
|
|
2128
|
+
setError(crash.recommendation || "Previous model load failed due to device memory limits.");
|
|
2129
|
+
onError?.(crash.recommendation || "Previous model load failed");
|
|
2130
|
+
}
|
|
2131
|
+
}, []);
|
|
2068
2132
|
useEffect(() => {
|
|
2069
2133
|
if (!shouldLoad || isReady) return;
|
|
2070
2134
|
mountedRef.current = true;
|
|
2135
|
+
setDownloadPhase("downloading", model);
|
|
2071
2136
|
setIsLoading(true);
|
|
2072
2137
|
setLoadingProgress({
|
|
2073
2138
|
status: "loading",
|
|
@@ -2093,6 +2158,7 @@ function useVoiceInput(options = {}) {
|
|
|
2093
2158
|
onProgress?.(progress);
|
|
2094
2159
|
}
|
|
2095
2160
|
if (type === "ready") {
|
|
2161
|
+
clearDownloadPhase();
|
|
2096
2162
|
setIsReady(true);
|
|
2097
2163
|
setIsLoading(false);
|
|
2098
2164
|
setLoadingProgress({ status: "ready" });
|
|
@@ -2109,6 +2175,7 @@ function useVoiceInput(options = {}) {
|
|
|
2109
2175
|
}
|
|
2110
2176
|
}
|
|
2111
2177
|
if (type === "error") {
|
|
2178
|
+
setDownloadPhase("error", model);
|
|
2112
2179
|
const errMsg = payload;
|
|
2113
2180
|
setError(errMsg);
|
|
2114
2181
|
setIsLoading(false);
|
|
@@ -2131,6 +2198,7 @@ function useVoiceInput(options = {}) {
|
|
|
2131
2198
|
};
|
|
2132
2199
|
worker.onerror = (err) => {
|
|
2133
2200
|
if (!mountedRef.current) return;
|
|
2201
|
+
setDownloadPhase("error", model);
|
|
2134
2202
|
let errMsg = err.message || "";
|
|
2135
2203
|
if (!errMsg || errMsg === "Script error.") errMsg = getWebGPUErrorMessage();
|
|
2136
2204
|
setError(errMsg);
|
|
@@ -3052,6 +3120,13 @@ function useEmbedding(options = {}) {
|
|
|
3052
3120
|
if (magnitude === 0) return 0;
|
|
3053
3121
|
return dotProduct / magnitude;
|
|
3054
3122
|
}, []);
|
|
3123
|
+
useEffect(() => {
|
|
3124
|
+
const crash = detectMemoryCrash();
|
|
3125
|
+
if (crash.crashed) {
|
|
3126
|
+
setError(crash.recommendation || "Previous model load failed due to device memory limits.");
|
|
3127
|
+
onError?.(crash.recommendation || "Previous model load failed");
|
|
3128
|
+
}
|
|
3129
|
+
}, []);
|
|
3055
3130
|
const load = useCallback(() => {
|
|
3056
3131
|
if (isReady && workerRef.current) return Promise.resolve();
|
|
3057
3132
|
if (loadRequestedRef.current && readyPromiseRef.current) return readyPromiseRef.current;
|
|
@@ -3061,6 +3136,7 @@ function useEmbedding(options = {}) {
|
|
|
3061
3136
|
status: "loading",
|
|
3062
3137
|
message: "Loading embedding model..."
|
|
3063
3138
|
});
|
|
3139
|
+
setDownloadPhase("downloading", model);
|
|
3064
3140
|
readyPromiseRef.current = new Promise((resolve) => {
|
|
3065
3141
|
readyResolveRef.current = resolve;
|
|
3066
3142
|
});
|
|
@@ -3075,18 +3151,21 @@ function useEmbedding(options = {}) {
|
|
|
3075
3151
|
progress: Math.round(payload.loaded / payload.total * 100)
|
|
3076
3152
|
});
|
|
3077
3153
|
} else if (type === "ready") {
|
|
3154
|
+
clearDownloadPhase();
|
|
3078
3155
|
setIsLoading(false);
|
|
3079
3156
|
setIsReady(true);
|
|
3080
3157
|
setLoadingProgress({ status: "ready" });
|
|
3081
3158
|
readyResolveRef.current?.();
|
|
3082
3159
|
onReady?.();
|
|
3083
3160
|
} else if (type === "error") {
|
|
3161
|
+
setDownloadPhase("error", model);
|
|
3084
3162
|
setIsLoading(false);
|
|
3085
3163
|
setError(payload);
|
|
3086
3164
|
onError?.(payload);
|
|
3087
3165
|
}
|
|
3088
3166
|
});
|
|
3089
3167
|
worker.onerror = (err) => {
|
|
3168
|
+
setDownloadPhase("error", model);
|
|
3090
3169
|
setIsLoading(false);
|
|
3091
3170
|
let errMsg = err.message || "";
|
|
3092
3171
|
if (!errMsg || errMsg === "Script error.") errMsg = getWebGPUErrorMessage();
|
|
@@ -3591,11 +3670,11 @@ async function getBrowserDiagnostics() {
|
|
|
3591
3670
|
} catch {
|
|
3592
3671
|
moduleWorkers = false;
|
|
3593
3672
|
}
|
|
3594
|
-
let indexedDB = false;
|
|
3673
|
+
let indexedDB$1 = false;
|
|
3595
3674
|
try {
|
|
3596
|
-
indexedDB = typeof window !== "undefined" && "indexedDB" in window;
|
|
3675
|
+
indexedDB$1 = typeof window !== "undefined" && "indexedDB" in window;
|
|
3597
3676
|
} catch {
|
|
3598
|
-
indexedDB = false;
|
|
3677
|
+
indexedDB$1 = false;
|
|
3599
3678
|
}
|
|
3600
3679
|
return {
|
|
3601
3680
|
browser,
|
|
@@ -3608,7 +3687,7 @@ async function getBrowserDiagnostics() {
|
|
|
3608
3687
|
webgpuExpected,
|
|
3609
3688
|
webgpu,
|
|
3610
3689
|
moduleWorkers,
|
|
3611
|
-
indexedDB
|
|
3690
|
+
indexedDB: indexedDB$1
|
|
3612
3691
|
};
|
|
3613
3692
|
}
|
|
3614
3693
|
/**
|
|
@@ -3646,6 +3725,370 @@ function getRecommendedModels() {
|
|
|
3646
3725
|
};
|
|
3647
3726
|
}
|
|
3648
3727
|
/**
|
|
3728
|
+
* Maximum safe model sizes for iOS devices (in MB).
|
|
3729
|
+
* Based on WKWebView effective memory limit of ~200-400MB.
|
|
3730
|
+
*/
|
|
3731
|
+
const IOS_MODEL_LIMITS = {
|
|
3732
|
+
safe: ["smollm2-135m", "smollm2-360m"],
|
|
3733
|
+
risky: ["qwen3-0.6b"],
|
|
3734
|
+
blocked: ["qwen3-1.7b", "qwen3-4b"],
|
|
3735
|
+
maxBudgetMB: 350
|
|
3736
|
+
};
|
|
3737
|
+
/**
|
|
3738
|
+
* Check if a model is safe to load on the current device.
|
|
3739
|
+
* Returns guidance specific to iOS memory constraints.
|
|
3740
|
+
*/
|
|
3741
|
+
function isModelSafeForDevice(modelId) {
|
|
3742
|
+
const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
|
|
3743
|
+
const isIOS = /iPhone|iPad|iPod/.test(ua);
|
|
3744
|
+
const isIOSChrome = isIOS && /CriOS/.test(ua);
|
|
3745
|
+
const deviceMemory = typeof navigator !== "undefined" ? navigator.deviceMemory : null;
|
|
3746
|
+
const normalizedId = modelId.toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
3747
|
+
if (isIOS) {
|
|
3748
|
+
if (IOS_MODEL_LIMITS.blocked.some((m) => normalizedId.includes(m.toLowerCase().replace(/[^a-z0-9]/g, "-")))) return {
|
|
3749
|
+
safe: false,
|
|
3750
|
+
reason: `Model ${modelId} is too large for iOS devices${isIOSChrome ? " (iOS Chrome uses WKWebView, same limits as Safari)" : ""}. WKWebView memory limit (~300-400MB) will cause crashes.`,
|
|
3751
|
+
recommendation: "Use smollm2-360m or qwen3-0.6b on iOS. For larger models, use desktop.",
|
|
3752
|
+
maxSafeModel: "qwen3-0.6b"
|
|
3753
|
+
};
|
|
3754
|
+
if (IOS_MODEL_LIMITS.risky.some((m) => normalizedId.includes(m.toLowerCase().replace(/[^a-z0-9]/g, "-")))) {
|
|
3755
|
+
if (!(deviceMemory && deviceMemory >= 4)) return {
|
|
3756
|
+
safe: false,
|
|
3757
|
+
reason: `Model ${modelId} may crash on older iOS devices. Your device reports ${deviceMemory || "unknown"}GB RAM.`,
|
|
3758
|
+
recommendation: "Use smollm2-360m for reliable performance, or try on iPhone 14+ / iPad Pro.",
|
|
3759
|
+
maxSafeModel: "smollm2-360m"
|
|
3760
|
+
};
|
|
3761
|
+
return {
|
|
3762
|
+
safe: true,
|
|
3763
|
+
reason: `Model ${modelId} should work on your high-memory iOS device, but may be slow.`
|
|
3764
|
+
};
|
|
3765
|
+
}
|
|
3766
|
+
return {
|
|
3767
|
+
safe: true,
|
|
3768
|
+
reason: "Model is within iOS memory limits."
|
|
3769
|
+
};
|
|
3770
|
+
}
|
|
3771
|
+
if (/Android/.test(ua)) {
|
|
3772
|
+
if (normalizedId.includes("qwen3-4b") || normalizedId.includes("7b")) return {
|
|
3773
|
+
safe: false,
|
|
3774
|
+
reason: `Model ${modelId} is very large and may crash on Android devices.`,
|
|
3775
|
+
recommendation: "Use qwen3-1.7b or smaller on Android.",
|
|
3776
|
+
maxSafeModel: "qwen3-1.7b"
|
|
3777
|
+
};
|
|
3778
|
+
}
|
|
3779
|
+
return {
|
|
3780
|
+
safe: true,
|
|
3781
|
+
reason: "Desktop browser has sufficient memory."
|
|
3782
|
+
};
|
|
3783
|
+
}
|
|
3784
|
+
const SESSION_STORAGE_KEY = "gerbil_session_phase";
|
|
3785
|
+
/**
|
|
3786
|
+
* Generate a unique session ID for tracking across reloads.
|
|
3787
|
+
*/
|
|
3788
|
+
function generateSessionId() {
|
|
3789
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
3790
|
+
}
|
|
3791
|
+
/**
|
|
3792
|
+
* Get or create the current session ID.
|
|
3793
|
+
*/
|
|
3794
|
+
function getSessionId() {
|
|
3795
|
+
if (typeof localStorage === "undefined") return generateSessionId();
|
|
3796
|
+
let sessionId = sessionStorage.getItem("gerbil_session_id");
|
|
3797
|
+
if (!sessionId) {
|
|
3798
|
+
sessionId = generateSessionId();
|
|
3799
|
+
sessionStorage.setItem("gerbil_session_id", sessionId);
|
|
3800
|
+
}
|
|
3801
|
+
return sessionId;
|
|
3802
|
+
}
|
|
3803
|
+
/**
|
|
3804
|
+
* Set the current download/initialization phase.
|
|
3805
|
+
* Used to detect if a reload happened during a critical operation.
|
|
3806
|
+
*/
|
|
3807
|
+
function setDownloadPhase(phase, modelId, progress) {
|
|
3808
|
+
if (typeof localStorage === "undefined") return;
|
|
3809
|
+
const state = {
|
|
3810
|
+
phase,
|
|
3811
|
+
modelId: modelId || null,
|
|
3812
|
+
sessionId: getSessionId(),
|
|
3813
|
+
timestamp: Date.now(),
|
|
3814
|
+
bytesDownloaded: progress?.bytesDownloaded,
|
|
3815
|
+
totalBytes: progress?.totalBytes
|
|
3816
|
+
};
|
|
3817
|
+
localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(state));
|
|
3818
|
+
}
|
|
3819
|
+
/**
|
|
3820
|
+
* Get the last known download phase from storage.
|
|
3821
|
+
*/
|
|
3822
|
+
function getDownloadPhase() {
|
|
3823
|
+
if (typeof localStorage === "undefined") return null;
|
|
3824
|
+
try {
|
|
3825
|
+
const raw = localStorage.getItem(SESSION_STORAGE_KEY);
|
|
3826
|
+
if (!raw) return null;
|
|
3827
|
+
return JSON.parse(raw);
|
|
3828
|
+
} catch {
|
|
3829
|
+
return null;
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
/**
|
|
3833
|
+
* Detect if the page reloaded during a model download/initialization.
|
|
3834
|
+
* This typically indicates an iOS memory crash.
|
|
3835
|
+
*
|
|
3836
|
+
* @returns Detection result with recommended action
|
|
3837
|
+
*/
|
|
3838
|
+
function detectMemoryCrash() {
|
|
3839
|
+
const lastState = getDownloadPhase();
|
|
3840
|
+
const currentSessionId = getSessionId();
|
|
3841
|
+
if (!lastState) return { crashed: false };
|
|
3842
|
+
const wasInCriticalPhase = [
|
|
3843
|
+
"downloading",
|
|
3844
|
+
"caching",
|
|
3845
|
+
"initializing"
|
|
3846
|
+
].includes(lastState.phase);
|
|
3847
|
+
const sessionChanged = lastState.sessionId !== currentSessionId;
|
|
3848
|
+
const timeSinceCrash = Date.now() - lastState.timestamp;
|
|
3849
|
+
if (wasInCriticalPhase && sessionChanged && timeSinceCrash < 300 * 1e3) {
|
|
3850
|
+
localStorage.removeItem(SESSION_STORAGE_KEY);
|
|
3851
|
+
return {
|
|
3852
|
+
crashed: true,
|
|
3853
|
+
phase: lastState.phase,
|
|
3854
|
+
modelId: lastState.modelId || void 0,
|
|
3855
|
+
timeSinceCrash,
|
|
3856
|
+
recommendation: lastState.modelId?.includes("1.7b") ? "The model was too large for your device. Try smollm2-360m or qwen3-0.6b instead." : "Your device ran out of memory. Try a smaller model or use a desktop browser."
|
|
3857
|
+
};
|
|
3858
|
+
}
|
|
3859
|
+
return { crashed: false };
|
|
3860
|
+
}
|
|
3861
|
+
/**
|
|
3862
|
+
* Clear session phase (call when model loads successfully).
|
|
3863
|
+
*/
|
|
3864
|
+
function clearDownloadPhase() {
|
|
3865
|
+
if (typeof localStorage === "undefined") return;
|
|
3866
|
+
localStorage.removeItem(SESSION_STORAGE_KEY);
|
|
3867
|
+
}
|
|
3868
|
+
/** Chunk size for downloads: 1.5MB (safe for iOS IndexedDB transactions) */
|
|
3869
|
+
const CHUNK_SIZE_BYTES = 1.5 * 1024 * 1024;
|
|
3870
|
+
/** IndexedDB database name for chunked downloads */
|
|
3871
|
+
const DOWNLOAD_DB_NAME = "gerbil-model-chunks";
|
|
3872
|
+
const DOWNLOAD_DB_VERSION = 1;
|
|
3873
|
+
/**
|
|
3874
|
+
* Open (or create) the IndexedDB for chunked downloads.
|
|
3875
|
+
*/
|
|
3876
|
+
async function openDownloadDB() {
|
|
3877
|
+
return new Promise((resolve, reject) => {
|
|
3878
|
+
const request = indexedDB.open(DOWNLOAD_DB_NAME, DOWNLOAD_DB_VERSION);
|
|
3879
|
+
request.onerror = () => reject(/* @__PURE__ */ new Error(`Failed to open download DB: ${request.error?.message}`));
|
|
3880
|
+
request.onsuccess = () => resolve(request.result);
|
|
3881
|
+
request.onupgradeneeded = (event) => {
|
|
3882
|
+
const db = event.target.result;
|
|
3883
|
+
if (!db.objectStoreNames.contains("manifests")) db.createObjectStore("manifests", { keyPath: "modelId" });
|
|
3884
|
+
if (!db.objectStoreNames.contains("chunks")) db.createObjectStore("chunks");
|
|
3885
|
+
};
|
|
3886
|
+
});
|
|
3887
|
+
}
|
|
3888
|
+
/**
|
|
3889
|
+
* Get download manifest for a model.
|
|
3890
|
+
*/
|
|
3891
|
+
async function getManifest(db, modelId) {
|
|
3892
|
+
return new Promise((resolve, reject) => {
|
|
3893
|
+
const request = db.transaction("manifests", "readonly").objectStore("manifests").get(modelId);
|
|
3894
|
+
request.onerror = () => reject(/* @__PURE__ */ new Error(`Failed to get manifest: ${request.error?.message}`));
|
|
3895
|
+
request.onsuccess = () => resolve(request.result || null);
|
|
3896
|
+
});
|
|
3897
|
+
}
|
|
3898
|
+
/**
|
|
3899
|
+
* Save download manifest.
|
|
3900
|
+
*/
|
|
3901
|
+
async function saveManifest(db, manifest) {
|
|
3902
|
+
return new Promise((resolve, reject) => {
|
|
3903
|
+
const request = db.transaction("manifests", "readwrite").objectStore("manifests").put(manifest);
|
|
3904
|
+
request.onerror = () => reject(/* @__PURE__ */ new Error(`Failed to save manifest: ${request.error?.message}`));
|
|
3905
|
+
request.onsuccess = () => resolve();
|
|
3906
|
+
});
|
|
3907
|
+
}
|
|
3908
|
+
/**
|
|
3909
|
+
* Save a single chunk.
|
|
3910
|
+
*/
|
|
3911
|
+
async function saveChunk(db, modelId, chunkIndex, data) {
|
|
3912
|
+
return new Promise((resolve, reject) => {
|
|
3913
|
+
const store = db.transaction("chunks", "readwrite").objectStore("chunks");
|
|
3914
|
+
const key = `${modelId}-${chunkIndex}`;
|
|
3915
|
+
const request = store.put(data, key);
|
|
3916
|
+
request.onerror = () => reject(/* @__PURE__ */ new Error(`Failed to save chunk ${chunkIndex}: ${request.error?.message}`));
|
|
3917
|
+
request.onsuccess = () => resolve();
|
|
3918
|
+
});
|
|
3919
|
+
}
|
|
3920
|
+
/**
|
|
3921
|
+
* Get a single chunk.
|
|
3922
|
+
*/
|
|
3923
|
+
async function getChunk(db, modelId, chunkIndex) {
|
|
3924
|
+
return new Promise((resolve, reject) => {
|
|
3925
|
+
const store = db.transaction("chunks", "readonly").objectStore("chunks");
|
|
3926
|
+
const key = `${modelId}-${chunkIndex}`;
|
|
3927
|
+
const request = store.get(key);
|
|
3928
|
+
request.onerror = () => reject(/* @__PURE__ */ new Error(`Failed to get chunk ${chunkIndex}: ${request.error?.message}`));
|
|
3929
|
+
request.onsuccess = () => resolve(request.result || null);
|
|
3930
|
+
});
|
|
3931
|
+
}
|
|
3932
|
+
/**
|
|
3933
|
+
* Delete all chunks and manifest for a model.
|
|
3934
|
+
*/
|
|
3935
|
+
async function clearModelData(db, modelId) {
|
|
3936
|
+
const manifest = await getManifest(db, modelId);
|
|
3937
|
+
return new Promise((resolve, reject) => {
|
|
3938
|
+
const tx = db.transaction(["manifests", "chunks"], "readwrite");
|
|
3939
|
+
tx.objectStore("manifests").delete(modelId);
|
|
3940
|
+
if (manifest) {
|
|
3941
|
+
const totalChunks = Math.ceil(manifest.totalBytes / manifest.chunkSize);
|
|
3942
|
+
const chunkStore = tx.objectStore("chunks");
|
|
3943
|
+
for (let i = 0; i < totalChunks; i++) chunkStore.delete(`${modelId}-${i}`);
|
|
3944
|
+
}
|
|
3945
|
+
tx.oncomplete = () => resolve();
|
|
3946
|
+
tx.onerror = () => reject(/* @__PURE__ */ new Error(`Failed to clear model data: ${tx.error?.message}`));
|
|
3947
|
+
});
|
|
3948
|
+
}
|
|
3949
|
+
/**
|
|
3950
|
+
* Chunked resumable downloader for large model files.
|
|
3951
|
+
* Downloads in 1.5MB chunks to avoid iOS memory pressure.
|
|
3952
|
+
*/
|
|
3953
|
+
async function downloadModelChunked(url, modelId, options = {}) {
|
|
3954
|
+
const { onProgress, signal } = options;
|
|
3955
|
+
setDownloadPhase("downloading", modelId);
|
|
3956
|
+
const db = await openDownloadDB();
|
|
3957
|
+
try {
|
|
3958
|
+
let manifest = await getManifest(db, modelId);
|
|
3959
|
+
const headResponse = await fetch(url, {
|
|
3960
|
+
method: "HEAD",
|
|
3961
|
+
signal
|
|
3962
|
+
});
|
|
3963
|
+
if (!headResponse.ok) throw new Error(`HEAD request failed: ${headResponse.status} ${headResponse.statusText}`);
|
|
3964
|
+
const contentLength = parseInt(headResponse.headers.get("content-length") || "0", 10);
|
|
3965
|
+
const etag = headResponse.headers.get("etag");
|
|
3966
|
+
const acceptRanges = headResponse.headers.get("accept-ranges");
|
|
3967
|
+
if (!contentLength) throw new Error("Server did not provide content-length");
|
|
3968
|
+
if (manifest && manifest.etag !== etag) {
|
|
3969
|
+
console.warn(`Model ${modelId} has been updated (etag mismatch). Clearing cached chunks.`);
|
|
3970
|
+
await clearModelData(db, modelId);
|
|
3971
|
+
manifest = null;
|
|
3972
|
+
}
|
|
3973
|
+
if (!(acceptRanges === "bytes")) {
|
|
3974
|
+
console.warn(`Server doesn't support range requests for ${modelId}. Using regular download.`);
|
|
3975
|
+
db.close();
|
|
3976
|
+
const response = await fetch(url, { signal });
|
|
3977
|
+
if (!response.ok) throw new Error(`Download failed: ${response.status}`);
|
|
3978
|
+
setDownloadPhase("caching", modelId);
|
|
3979
|
+
const buffer = await response.arrayBuffer();
|
|
3980
|
+
setDownloadPhase("ready", modelId);
|
|
3981
|
+
return buffer;
|
|
3982
|
+
}
|
|
3983
|
+
const totalChunks = Math.ceil(contentLength / CHUNK_SIZE_BYTES);
|
|
3984
|
+
if (!manifest) {
|
|
3985
|
+
manifest = {
|
|
3986
|
+
modelId,
|
|
3987
|
+
url,
|
|
3988
|
+
etag,
|
|
3989
|
+
totalBytes: contentLength,
|
|
3990
|
+
chunkSize: CHUNK_SIZE_BYTES,
|
|
3991
|
+
completedChunks: [],
|
|
3992
|
+
createdAt: Date.now(),
|
|
3993
|
+
updatedAt: Date.now()
|
|
3994
|
+
};
|
|
3995
|
+
await saveManifest(db, manifest);
|
|
3996
|
+
}
|
|
3997
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
3998
|
+
if (signal?.aborted) throw new Error("Download aborted");
|
|
3999
|
+
if (manifest.completedChunks.includes(i)) {
|
|
4000
|
+
const bytesDownloaded$1 = manifest.completedChunks.length / totalChunks * contentLength;
|
|
4001
|
+
onProgress?.({
|
|
4002
|
+
phase: "resuming",
|
|
4003
|
+
bytesDownloaded: bytesDownloaded$1,
|
|
4004
|
+
totalBytes: contentLength,
|
|
4005
|
+
percent: Math.round(bytesDownloaded$1 / contentLength * 100)
|
|
4006
|
+
});
|
|
4007
|
+
continue;
|
|
4008
|
+
}
|
|
4009
|
+
const start = i * CHUNK_SIZE_BYTES;
|
|
4010
|
+
const end = Math.min(start + CHUNK_SIZE_BYTES - 1, contentLength - 1);
|
|
4011
|
+
const response = await fetch(url, {
|
|
4012
|
+
headers: { Range: `bytes=${start}-${end}` },
|
|
4013
|
+
signal
|
|
4014
|
+
});
|
|
4015
|
+
if (response.status !== 206) throw new Error(`Range request failed: ${response.status} (expected 206)`);
|
|
4016
|
+
const chunkData = await response.arrayBuffer();
|
|
4017
|
+
await saveChunk(db, modelId, i, chunkData);
|
|
4018
|
+
manifest.completedChunks.push(i);
|
|
4019
|
+
manifest.updatedAt = Date.now();
|
|
4020
|
+
await saveManifest(db, manifest);
|
|
4021
|
+
const bytesDownloaded = manifest.completedChunks.length * CHUNK_SIZE_BYTES;
|
|
4022
|
+
setDownloadPhase("downloading", modelId, {
|
|
4023
|
+
bytesDownloaded,
|
|
4024
|
+
totalBytes: contentLength
|
|
4025
|
+
});
|
|
4026
|
+
onProgress?.({
|
|
4027
|
+
phase: "downloading",
|
|
4028
|
+
bytesDownloaded: Math.min(bytesDownloaded, contentLength),
|
|
4029
|
+
totalBytes: contentLength,
|
|
4030
|
+
percent: Math.round(manifest.completedChunks.length / totalChunks * 100)
|
|
4031
|
+
});
|
|
4032
|
+
response.body = null;
|
|
4033
|
+
}
|
|
4034
|
+
setDownloadPhase("caching", modelId);
|
|
4035
|
+
onProgress?.({
|
|
4036
|
+
phase: "assembling",
|
|
4037
|
+
bytesDownloaded: contentLength,
|
|
4038
|
+
totalBytes: contentLength,
|
|
4039
|
+
percent: 100
|
|
4040
|
+
});
|
|
4041
|
+
const finalBuffer = new ArrayBuffer(contentLength);
|
|
4042
|
+
const finalView = new Uint8Array(finalBuffer);
|
|
4043
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
4044
|
+
const chunk = await getChunk(db, modelId, i);
|
|
4045
|
+
if (!chunk) throw new Error(`Missing chunk ${i} during assembly`);
|
|
4046
|
+
const offset = i * CHUNK_SIZE_BYTES;
|
|
4047
|
+
finalView.set(new Uint8Array(chunk), offset);
|
|
4048
|
+
}
|
|
4049
|
+
await clearModelData(db, modelId);
|
|
4050
|
+
db.close();
|
|
4051
|
+
setDownloadPhase("ready", modelId);
|
|
4052
|
+
return finalBuffer;
|
|
4053
|
+
} catch (error) {
|
|
4054
|
+
setDownloadPhase("error", modelId);
|
|
4055
|
+
db.close();
|
|
4056
|
+
throw error;
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
/**
|
|
4060
|
+
* Check if a model has an incomplete download.
|
|
4061
|
+
*/
|
|
4062
|
+
async function hasIncompleteDownload(modelId) {
|
|
4063
|
+
try {
|
|
4064
|
+
const db = await openDownloadDB();
|
|
4065
|
+
const manifest = await getManifest(db, modelId);
|
|
4066
|
+
db.close();
|
|
4067
|
+
if (!manifest) return { incomplete: false };
|
|
4068
|
+
const totalChunks = Math.ceil(manifest.totalBytes / manifest.chunkSize);
|
|
4069
|
+
const completedChunks = manifest.completedChunks.length;
|
|
4070
|
+
if (completedChunks < totalChunks) return {
|
|
4071
|
+
incomplete: true,
|
|
4072
|
+
bytesDownloaded: completedChunks * manifest.chunkSize,
|
|
4073
|
+
totalBytes: manifest.totalBytes,
|
|
4074
|
+
percent: Math.round(completedChunks / totalChunks * 100)
|
|
4075
|
+
};
|
|
4076
|
+
return { incomplete: false };
|
|
4077
|
+
} catch {
|
|
4078
|
+
return { incomplete: false };
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
/**
|
|
4082
|
+
* Clear incomplete download data for a model.
|
|
4083
|
+
*/
|
|
4084
|
+
async function clearIncompleteDownload(modelId) {
|
|
4085
|
+
try {
|
|
4086
|
+
const db = await openDownloadDB();
|
|
4087
|
+
await clearModelData(db, modelId);
|
|
4088
|
+
db.close();
|
|
4089
|
+
} catch {}
|
|
4090
|
+
}
|
|
4091
|
+
/**
|
|
3649
4092
|
* Check if there's enough storage quota for a model download.
|
|
3650
4093
|
* Returns estimated available space and whether download should proceed.
|
|
3651
4094
|
*/
|
|
@@ -3693,6 +4136,14 @@ var browser_default = {
|
|
|
3693
4136
|
getBrowserDiagnostics,
|
|
3694
4137
|
getRecommendedModels,
|
|
3695
4138
|
checkStorageQuota,
|
|
4139
|
+
isModelSafeForDevice,
|
|
4140
|
+
setDownloadPhase,
|
|
4141
|
+
getDownloadPhase,
|
|
4142
|
+
detectMemoryCrash,
|
|
4143
|
+
clearDownloadPhase,
|
|
4144
|
+
downloadModelChunked,
|
|
4145
|
+
hasIncompleteDownload,
|
|
4146
|
+
clearIncompleteDownload,
|
|
3696
4147
|
createGerbilWorker,
|
|
3697
4148
|
playAudio,
|
|
3698
4149
|
createAudioPlayer,
|
|
@@ -3703,5 +4154,5 @@ var browser_default = {
|
|
|
3703
4154
|
};
|
|
3704
4155
|
|
|
3705
4156
|
//#endregion
|
|
3706
|
-
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 };
|
|
4157
|
+
export { BUILTIN_MODELS, checkStorageQuota, checkWebGPUCapabilities, checkWebGPUReady, clearDownloadPhase, clearIncompleteDownload, createAudioPlayer, createGerbilWorker, browser_default as default, detectMemoryCrash, downloadModelChunked, getBrowserDiagnostics, getDownloadPhase, getRecommendedModels, getWebGPUInfo, hasIncompleteDownload, isModelSafeForDevice, isWebGPUSupported, playAudio, preloadChatModel, preloadEmbeddingModel, preloadSTTModel, preloadTTSModel, setDownloadPhase, useChat, useCompletion, useEmbedding, useSpeech, useVoiceChat, useVoiceInput };
|
|
3707
4158
|
//# sourceMappingURL=index.js.map
|