@runanywhere/web 0.1.0-beta.6 → 0.1.0-beta.7
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/Foundation/ErrorTypes.d.ts +1 -0
- package/dist/Foundation/ErrorTypes.d.ts.map +1 -1
- package/dist/Foundation/ErrorTypes.js +3 -0
- package/dist/Foundation/ErrorTypes.js.map +1 -1
- package/dist/Foundation/EventBus.d.ts +0 -1
- package/dist/Foundation/EventBus.d.ts.map +1 -1
- package/dist/Foundation/StructOffsets.d.ts +5 -37
- package/dist/Foundation/StructOffsets.d.ts.map +1 -1
- package/dist/Foundation/StructOffsets.js +6 -157
- package/dist/Foundation/StructOffsets.js.map +1 -1
- package/dist/Foundation/WASMBridge.d.ts +8 -236
- package/dist/Foundation/WASMBridge.d.ts.map +1 -1
- package/dist/Foundation/WASMBridge.js +7 -388
- package/dist/Foundation/WASMBridge.js.map +1 -1
- package/dist/Infrastructure/DeviceCapabilities.d.ts.map +1 -1
- package/dist/Infrastructure/DeviceCapabilities.js +1 -3
- package/dist/Infrastructure/DeviceCapabilities.js.map +1 -1
- package/dist/Infrastructure/ExtensionPoint.d.ts +114 -0
- package/dist/Infrastructure/ExtensionPoint.d.ts.map +1 -0
- package/dist/Infrastructure/ExtensionPoint.js +178 -0
- package/dist/Infrastructure/ExtensionPoint.js.map +1 -0
- package/dist/Infrastructure/LocalFileStorage.d.ts +134 -0
- package/dist/Infrastructure/LocalFileStorage.d.ts.map +1 -0
- package/dist/Infrastructure/LocalFileStorage.js +428 -0
- package/dist/Infrastructure/LocalFileStorage.js.map +1 -0
- package/dist/Infrastructure/ModelDownloader.d.ts +21 -5
- package/dist/Infrastructure/ModelDownloader.d.ts.map +1 -1
- package/dist/Infrastructure/ModelDownloader.js +79 -7
- package/dist/Infrastructure/ModelDownloader.js.map +1 -1
- package/dist/Infrastructure/ModelFileInference.d.ts +39 -0
- package/dist/Infrastructure/ModelFileInference.d.ts.map +1 -0
- package/dist/Infrastructure/ModelFileInference.js +119 -0
- package/dist/Infrastructure/ModelFileInference.js.map +1 -0
- package/dist/Infrastructure/ModelLoaderTypes.d.ts +91 -12
- package/dist/Infrastructure/ModelLoaderTypes.d.ts.map +1 -1
- package/dist/Infrastructure/ModelLoaderTypes.js +7 -1
- package/dist/Infrastructure/ModelLoaderTypes.js.map +1 -1
- package/dist/Infrastructure/ModelManager.d.ts +31 -104
- package/dist/Infrastructure/ModelManager.d.ts.map +1 -1
- package/dist/Infrastructure/ModelManager.js +207 -568
- package/dist/Infrastructure/ModelManager.js.map +1 -1
- package/dist/Infrastructure/ModelRegistry.d.ts +6 -8
- package/dist/Infrastructure/ModelRegistry.d.ts.map +1 -1
- package/dist/Infrastructure/ModelRegistry.js +11 -4
- package/dist/Infrastructure/ModelRegistry.js.map +1 -1
- package/dist/Infrastructure/OPFSStorage.d.ts +8 -0
- package/dist/Infrastructure/OPFSStorage.d.ts.map +1 -1
- package/dist/Infrastructure/OPFSStorage.js +37 -0
- package/dist/Infrastructure/OPFSStorage.js.map +1 -1
- package/dist/Public/Extensions/RunAnywhere+ModelManagement.d.ts +12 -4
- package/dist/Public/Extensions/RunAnywhere+ModelManagement.d.ts.map +1 -1
- package/dist/Public/Extensions/RunAnywhere+ModelManagement.js +23 -51
- package/dist/Public/Extensions/RunAnywhere+ModelManagement.js.map +1 -1
- package/dist/Public/Extensions/RunAnywhere+VoiceAgent.d.ts +42 -10
- package/dist/Public/Extensions/RunAnywhere+VoiceAgent.d.ts.map +1 -1
- package/dist/Public/Extensions/RunAnywhere+VoiceAgent.js +63 -161
- package/dist/Public/Extensions/RunAnywhere+VoiceAgent.js.map +1 -1
- package/dist/Public/Extensions/RunAnywhere+VoicePipeline.d.ts +3 -29
- package/dist/Public/Extensions/RunAnywhere+VoicePipeline.d.ts.map +1 -1
- package/dist/Public/Extensions/RunAnywhere+VoicePipeline.js +26 -42
- package/dist/Public/Extensions/RunAnywhere+VoicePipeline.js.map +1 -1
- package/dist/Public/Extensions/VoicePipelineTypes.d.ts +28 -37
- package/dist/Public/Extensions/VoicePipelineTypes.d.ts.map +1 -1
- package/dist/Public/Extensions/VoicePipelineTypes.js +4 -1
- package/dist/Public/Extensions/VoicePipelineTypes.js.map +1 -1
- package/dist/Public/RunAnywhere.d.ts +29 -85
- package/dist/Public/RunAnywhere.d.ts.map +1 -1
- package/dist/Public/RunAnywhere.js +169 -211
- package/dist/Public/RunAnywhere.js.map +1 -1
- package/dist/index.d.ts +19 -39
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -31
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/package.json +4 -10
- package/dist/Foundation/PlatformAdapter.d.ts +0 -101
- package/dist/Foundation/PlatformAdapter.d.ts.map +0 -1
- package/dist/Foundation/PlatformAdapter.js +0 -417
- package/dist/Foundation/PlatformAdapter.js.map +0 -1
- package/dist/Foundation/SherpaONNXBridge.d.ts +0 -147
- package/dist/Foundation/SherpaONNXBridge.d.ts.map +0 -1
- package/dist/Foundation/SherpaONNXBridge.js +0 -345
- package/dist/Foundation/SherpaONNXBridge.js.map +0 -1
- package/dist/Infrastructure/AudioCapture.d.ts +0 -99
- package/dist/Infrastructure/AudioCapture.d.ts.map +0 -1
- package/dist/Infrastructure/AudioCapture.js +0 -264
- package/dist/Infrastructure/AudioCapture.js.map +0 -1
- package/dist/Infrastructure/AudioPlayback.d.ts +0 -53
- package/dist/Infrastructure/AudioPlayback.d.ts.map +0 -1
- package/dist/Infrastructure/AudioPlayback.js +0 -117
- package/dist/Infrastructure/AudioPlayback.js.map +0 -1
- package/dist/Infrastructure/VLMWorkerBridge.d.ts +0 -211
- package/dist/Infrastructure/VLMWorkerBridge.d.ts.map +0 -1
- package/dist/Infrastructure/VLMWorkerBridge.js +0 -264
- package/dist/Infrastructure/VLMWorkerBridge.js.map +0 -1
- package/dist/Infrastructure/VLMWorkerRuntime.d.ts +0 -38
- package/dist/Infrastructure/VLMWorkerRuntime.d.ts.map +0 -1
- package/dist/Infrastructure/VLMWorkerRuntime.js +0 -503
- package/dist/Infrastructure/VLMWorkerRuntime.js.map +0 -1
- package/dist/Infrastructure/VideoCapture.d.ts +0 -130
- package/dist/Infrastructure/VideoCapture.d.ts.map +0 -1
- package/dist/Infrastructure/VideoCapture.js +0 -236
- package/dist/Infrastructure/VideoCapture.js.map +0 -1
- package/dist/Public/Extensions/DiffusionTypes.d.ts +0 -64
- package/dist/Public/Extensions/DiffusionTypes.d.ts.map +0 -1
- package/dist/Public/Extensions/DiffusionTypes.js +0 -28
- package/dist/Public/Extensions/DiffusionTypes.js.map +0 -1
- package/dist/Public/Extensions/EmbeddingsTypes.d.ts +0 -33
- package/dist/Public/Extensions/EmbeddingsTypes.d.ts.map +0 -1
- package/dist/Public/Extensions/EmbeddingsTypes.js +0 -13
- package/dist/Public/Extensions/EmbeddingsTypes.js.map +0 -1
- package/dist/Public/Extensions/RunAnywhere+Diffusion.d.ts +0 -44
- package/dist/Public/Extensions/RunAnywhere+Diffusion.d.ts.map +0 -1
- package/dist/Public/Extensions/RunAnywhere+Diffusion.js +0 -189
- package/dist/Public/Extensions/RunAnywhere+Diffusion.js.map +0 -1
- package/dist/Public/Extensions/RunAnywhere+Embeddings.d.ts +0 -56
- package/dist/Public/Extensions/RunAnywhere+Embeddings.d.ts.map +0 -1
- package/dist/Public/Extensions/RunAnywhere+Embeddings.js +0 -240
- package/dist/Public/Extensions/RunAnywhere+Embeddings.js.map +0 -1
- package/dist/Public/Extensions/RunAnywhere+STT.d.ts +0 -97
- package/dist/Public/Extensions/RunAnywhere+STT.d.ts.map +0 -1
- package/dist/Public/Extensions/RunAnywhere+STT.js +0 -417
- package/dist/Public/Extensions/RunAnywhere+STT.js.map +0 -1
- package/dist/Public/Extensions/RunAnywhere+StructuredOutput.d.ts +0 -69
- package/dist/Public/Extensions/RunAnywhere+StructuredOutput.d.ts.map +0 -1
- package/dist/Public/Extensions/RunAnywhere+StructuredOutput.js +0 -196
- package/dist/Public/Extensions/RunAnywhere+StructuredOutput.js.map +0 -1
- package/dist/Public/Extensions/RunAnywhere+TTS.d.ts +0 -55
- package/dist/Public/Extensions/RunAnywhere+TTS.d.ts.map +0 -1
- package/dist/Public/Extensions/RunAnywhere+TTS.js +0 -253
- package/dist/Public/Extensions/RunAnywhere+TTS.js.map +0 -1
- package/dist/Public/Extensions/RunAnywhere+TextGeneration.d.ts +0 -80
- package/dist/Public/Extensions/RunAnywhere+TextGeneration.d.ts.map +0 -1
- package/dist/Public/Extensions/RunAnywhere+TextGeneration.js +0 -470
- package/dist/Public/Extensions/RunAnywhere+TextGeneration.js.map +0 -1
- package/dist/Public/Extensions/RunAnywhere+ToolCalling.d.ts +0 -82
- package/dist/Public/Extensions/RunAnywhere+ToolCalling.d.ts.map +0 -1
- package/dist/Public/Extensions/RunAnywhere+ToolCalling.js +0 -576
- package/dist/Public/Extensions/RunAnywhere+ToolCalling.js.map +0 -1
- package/dist/Public/Extensions/RunAnywhere+VAD.d.ts +0 -70
- package/dist/Public/Extensions/RunAnywhere+VAD.d.ts.map +0 -1
- package/dist/Public/Extensions/RunAnywhere+VAD.js +0 -231
- package/dist/Public/Extensions/RunAnywhere+VAD.js.map +0 -1
- package/dist/Public/Extensions/RunAnywhere+VLM.d.ts +0 -58
- package/dist/Public/Extensions/RunAnywhere+VLM.d.ts.map +0 -1
- package/dist/Public/Extensions/RunAnywhere+VLM.js +0 -262
- package/dist/Public/Extensions/RunAnywhere+VLM.js.map +0 -1
- package/dist/Public/Extensions/STTTypes.d.ts +0 -61
- package/dist/Public/Extensions/STTTypes.d.ts.map +0 -1
- package/dist/Public/Extensions/STTTypes.js +0 -16
- package/dist/Public/Extensions/STTTypes.js.map +0 -1
- package/dist/Public/Extensions/TTSTypes.d.ts +0 -31
- package/dist/Public/Extensions/TTSTypes.d.ts.map +0 -1
- package/dist/Public/Extensions/TTSTypes.js +0 -3
- package/dist/Public/Extensions/TTSTypes.js.map +0 -1
- package/dist/Public/Extensions/ToolCallingTypes.d.ts +0 -78
- package/dist/Public/Extensions/ToolCallingTypes.d.ts.map +0 -1
- package/dist/Public/Extensions/ToolCallingTypes.js +0 -8
- package/dist/Public/Extensions/ToolCallingTypes.js.map +0 -1
- package/dist/Public/Extensions/VADTypes.d.ts +0 -30
- package/dist/Public/Extensions/VADTypes.d.ts.map +0 -1
- package/dist/Public/Extensions/VADTypes.js +0 -8
- package/dist/Public/Extensions/VADTypes.js.map +0 -1
- package/dist/Public/Extensions/VLMTypes.d.ts +0 -56
- package/dist/Public/Extensions/VLMTypes.d.ts.map +0 -1
- package/dist/Public/Extensions/VLMTypes.js +0 -24
- package/dist/Public/Extensions/VLMTypes.js.map +0 -1
- package/dist/types/LLMTypes.d.ts +0 -48
- package/dist/types/LLMTypes.d.ts.map +0 -1
- package/dist/types/LLMTypes.js +0 -8
- package/dist/types/LLMTypes.js.map +0 -1
- package/dist/workers/vlm-worker.d.ts +0 -9
- package/dist/workers/vlm-worker.d.ts.map +0 -1
- package/dist/workers/vlm-worker.js +0 -10
- package/dist/workers/vlm-worker.js.map +0 -1
- package/wasm/racommons-webgpu.js +0 -156
- package/wasm/racommons-webgpu.wasm +0 -0
- package/wasm/racommons.js +0 -126
- package/wasm/racommons.wasm +0 -0
- package/wasm/sherpa/sherpa-onnx-asr.js +0 -1538
- package/wasm/sherpa/sherpa-onnx-glue-original.js +0 -19
- package/wasm/sherpa/sherpa-onnx-glue.js +0 -17
- package/wasm/sherpa/sherpa-onnx-tts.js +0 -657
- package/wasm/sherpa/sherpa-onnx-vad.js +0 -337
- package/wasm/sherpa/sherpa-onnx-wave.js +0 -88
- package/wasm/sherpa/sherpa-onnx.wasm +0 -0
|
@@ -4,21 +4,17 @@
|
|
|
4
4
|
* Composes ModelRegistry (catalog) + ModelDownloader (downloads) and adds
|
|
5
5
|
* model-loading orchestration (STT / TTS / LLM / VLM routing).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* operations to the Downloader.
|
|
7
|
+
* Backend-specific logic (writing to sherpa-onnx FS, extracting archives,
|
|
8
|
+
* creating recognizer configs) is handled by the pluggable loader interfaces.
|
|
9
|
+
* This keeps ModelManager backend-agnostic — it only depends on core types.
|
|
11
10
|
*/
|
|
12
|
-
import { WASMBridge } from '../Foundation/WASMBridge';
|
|
13
|
-
import { SherpaONNXBridge } from '../Foundation/SherpaONNXBridge';
|
|
14
11
|
import { EventBus } from '../Foundation/EventBus';
|
|
15
12
|
import { SDKLogger } from '../Foundation/SDKLogger';
|
|
16
|
-
import { STTModelType } from '../Public/Extensions/STTTypes';
|
|
17
13
|
import { ModelCategory, LLMFramework, ModelStatus, DownloadStage, SDKEventType } from '../types/enums';
|
|
18
14
|
import { OPFSStorage } from './OPFSStorage';
|
|
19
15
|
import { ModelRegistry } from './ModelRegistry';
|
|
20
16
|
import { ModelDownloader } from './ModelDownloader';
|
|
21
|
-
import {
|
|
17
|
+
import { inferModelFromFilename, sanitizeId } from './ModelFileInference';
|
|
22
18
|
// Re-export types so existing imports from './Infrastructure/ModelManager' still work
|
|
23
19
|
export { ModelCategory, LLMFramework, ModelStatus, DownloadStage };
|
|
24
20
|
// ---------------------------------------------------------------------------
|
|
@@ -38,48 +34,38 @@ class ModelManagerImpl {
|
|
|
38
34
|
metadata = {};
|
|
39
35
|
/** Pluggable VLM loader (set by the app via setVLMLoader) */
|
|
40
36
|
vlmLoader = null;
|
|
41
|
-
/** Pluggable model loaders — registered by
|
|
37
|
+
/** Pluggable model loaders — registered by backend providers */
|
|
42
38
|
llmLoader = null;
|
|
43
39
|
sttLoader = null;
|
|
44
40
|
ttsLoader = null;
|
|
45
41
|
vadLoader = null;
|
|
46
42
|
constructor() {
|
|
47
43
|
this.downloader = new ModelDownloader(this.registry, this.storage);
|
|
48
|
-
// Initialize OPFS storage (non-blocking)
|
|
49
44
|
this.initStorage();
|
|
50
|
-
// Request persistent storage so browser won't evict our cached models
|
|
51
45
|
this.requestPersistentStorage();
|
|
52
46
|
}
|
|
53
47
|
async initStorage() {
|
|
54
48
|
await this.storage.initialize();
|
|
55
49
|
}
|
|
56
|
-
// --- Registration API
|
|
57
|
-
/**
|
|
58
|
-
* Register a catalog of models. Resolves compact definitions into full
|
|
59
|
-
* ManagedModel entries and checks OPFS for previously downloaded files.
|
|
60
|
-
*/
|
|
50
|
+
// --- Registration API ---
|
|
61
51
|
registerModels(models) {
|
|
62
52
|
this.registry.registerModels(models);
|
|
63
|
-
// Check OPFS for previously downloaded models (async, updates status when done)
|
|
64
53
|
this.refreshDownloadStatus();
|
|
65
54
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Set the VLM loader implementation. Called by the app to plug in
|
|
68
|
-
* worker-based VLM loading (the SDK doesn't create Web Workers directly).
|
|
69
|
-
*/
|
|
70
55
|
setVLMLoader(loader) {
|
|
71
56
|
this.vlmLoader = loader;
|
|
72
57
|
}
|
|
73
|
-
/** Register the LLM model loader (text generation extension). */
|
|
74
58
|
setLLMLoader(loader) { this.llmLoader = loader; }
|
|
75
|
-
/** Register the STT model loader (speech-to-text extension). */
|
|
76
59
|
setSTTLoader(loader) { this.sttLoader = loader; }
|
|
77
|
-
/** Register the TTS model loader (text-to-speech extension). */
|
|
78
60
|
setTTSLoader(loader) { this.ttsLoader = loader; }
|
|
79
|
-
/** Register the VAD model loader (voice activity detection extension). */
|
|
80
61
|
setVADLoader(loader) { this.vadLoader = loader; }
|
|
62
|
+
/** Expose the downloader for backend packages that need file operations. */
|
|
63
|
+
getDownloader() { return this.downloader; }
|
|
64
|
+
/** Set the local file storage backend for persistent model storage. */
|
|
65
|
+
setLocalFileStorage(storage) {
|
|
66
|
+
this.downloader.setLocalFileStorage(storage);
|
|
67
|
+
}
|
|
81
68
|
// --- Internal init ---
|
|
82
|
-
/** Request persistent storage to prevent browser from evicting cached models */
|
|
83
69
|
async requestPersistentStorage() {
|
|
84
70
|
try {
|
|
85
71
|
if (navigator.storage?.persist) {
|
|
@@ -88,24 +74,18 @@ class ModelManagerImpl {
|
|
|
88
74
|
logger.info('Persistent storage: granted');
|
|
89
75
|
}
|
|
90
76
|
else {
|
|
91
|
-
// Expected on first visit — browsers require engagement signals
|
|
92
|
-
// (bookmark, PWA install, etc.) before granting persistence.
|
|
93
77
|
logger.debug('Persistent storage: denied (expected on first visit)');
|
|
94
78
|
}
|
|
95
79
|
}
|
|
96
80
|
}
|
|
97
81
|
catch {
|
|
98
|
-
// Not supported or denied
|
|
82
|
+
// Not supported or denied
|
|
99
83
|
}
|
|
100
84
|
}
|
|
101
|
-
/**
|
|
102
|
-
* Check OPFS for models that were downloaded in a previous session.
|
|
103
|
-
* Updates their status from 'registered' to 'downloaded'.
|
|
104
|
-
* Also loads persisted LRU metadata for each model.
|
|
105
|
-
* Only checks file existence + size — does NOT read file contents into memory.
|
|
106
|
-
*/
|
|
107
85
|
async refreshDownloadStatus() {
|
|
108
|
-
//
|
|
86
|
+
// Ensure OPFS is initialized before checking for previously downloaded models.
|
|
87
|
+
// initStorage() is idempotent — returns immediately if already done.
|
|
88
|
+
await this.storage.initialize();
|
|
109
89
|
this.metadata = await this.storage.loadMetadata();
|
|
110
90
|
for (const model of this.registry.getModels()) {
|
|
111
91
|
if (model.status !== ModelStatus.Registered)
|
|
@@ -114,7 +94,6 @@ class ModelManagerImpl {
|
|
|
114
94
|
const size = await this.downloader.getOPFSFileSize(model.id);
|
|
115
95
|
if (size !== null && size > 0) {
|
|
116
96
|
this.registry.updateModel(model.id, { status: ModelStatus.Downloaded, sizeBytes: size });
|
|
117
|
-
// Ensure metadata entry exists — use persisted value or fall back to OPFS lastModified
|
|
118
97
|
if (!this.metadata[model.id]) {
|
|
119
98
|
const stored = await this.storage.listModels();
|
|
120
99
|
const entry = stored.find((s) => s.id === model.id);
|
|
@@ -129,34 +108,17 @@ class ModelManagerImpl {
|
|
|
129
108
|
// Not in OPFS, keep as registered
|
|
130
109
|
}
|
|
131
110
|
}
|
|
132
|
-
// Persist any newly created metadata entries
|
|
133
111
|
await this.storage.saveMetadata(this.metadata);
|
|
134
112
|
}
|
|
135
|
-
// --- Queries
|
|
136
|
-
getModels() {
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
getLLMModels() {
|
|
146
|
-
return this.registry.getLLMModels();
|
|
147
|
-
}
|
|
148
|
-
getVLMModels() {
|
|
149
|
-
return this.registry.getVLMModels();
|
|
150
|
-
}
|
|
151
|
-
getSTTModels() {
|
|
152
|
-
return this.registry.getSTTModels();
|
|
153
|
-
}
|
|
154
|
-
getTTSModels() {
|
|
155
|
-
return this.registry.getTTSModels();
|
|
156
|
-
}
|
|
157
|
-
getVADModels() {
|
|
158
|
-
return this.registry.getVADModels();
|
|
159
|
-
}
|
|
113
|
+
// --- Queries ---
|
|
114
|
+
getModels() { return this.registry.getModels(); }
|
|
115
|
+
getModelsByCategory(category) { return this.registry.getModelsByCategory(category); }
|
|
116
|
+
getModelsByFramework(framework) { return this.registry.getModelsByFramework(framework); }
|
|
117
|
+
getLLMModels() { return this.registry.getLLMModels(); }
|
|
118
|
+
getVLMModels() { return this.registry.getVLMModels(); }
|
|
119
|
+
getSTTModels() { return this.registry.getSTTModels(); }
|
|
120
|
+
getTTSModels() { return this.registry.getTTSModels(); }
|
|
121
|
+
getVADModels() { return this.registry.getVADModels(); }
|
|
160
122
|
getLoadedModel(category) {
|
|
161
123
|
if (category) {
|
|
162
124
|
const id = this.loadedByCategory.get(category);
|
|
@@ -168,69 +130,92 @@ class ModelManagerImpl {
|
|
|
168
130
|
if (category) {
|
|
169
131
|
return this.loadedByCategory.get(category) ?? null;
|
|
170
132
|
}
|
|
171
|
-
// Legacy: return first loaded model id
|
|
172
133
|
return this.registry.getModels().find((m) => m.status === ModelStatus.Loaded)?.id ?? null;
|
|
173
134
|
}
|
|
174
|
-
/** Check if models for all given categories are loaded */
|
|
175
135
|
areAllLoaded(categories) {
|
|
176
136
|
return categories.every((c) => this.loadedByCategory.has(c));
|
|
177
137
|
}
|
|
178
|
-
/**
|
|
179
|
-
* Ensure a model is loaded for the given category.
|
|
180
|
-
* If already loaded, returns the loaded model. If a downloaded model exists,
|
|
181
|
-
* loads it automatically. Returns null if no suitable model is available.
|
|
182
|
-
*
|
|
183
|
-
* @param options.coexist Forwarded to `loadModel()`. When true, only swaps
|
|
184
|
-
* models of the same category instead of unloading everything.
|
|
185
|
-
*/
|
|
186
138
|
async ensureLoaded(category, options) {
|
|
187
|
-
// Check if already loaded
|
|
188
139
|
const loaded = this.getLoadedModel(category);
|
|
189
140
|
if (loaded)
|
|
190
141
|
return loaded;
|
|
191
|
-
// Find a downloaded model for this category
|
|
192
142
|
const models = this.getModels();
|
|
193
143
|
const downloaded = models.find(m => m.modality === category && m.status === ModelStatus.Downloaded);
|
|
194
144
|
if (!downloaded)
|
|
195
145
|
return null;
|
|
196
|
-
// Load it
|
|
197
146
|
await this.loadModel(downloaded.id, options);
|
|
198
147
|
return this.getLoadedModel(category);
|
|
199
148
|
}
|
|
200
|
-
// --- Download
|
|
201
|
-
/**
|
|
202
|
-
* Check whether downloading a model will fit in OPFS without eviction.
|
|
203
|
-
* Returns a result indicating whether it fits and which models could be
|
|
204
|
-
* evicted if not. Does NOT perform any mutations.
|
|
205
|
-
*/
|
|
149
|
+
// --- Download ---
|
|
206
150
|
async checkDownloadFit(modelId) {
|
|
207
151
|
const model = this.registry.getModel(modelId);
|
|
208
152
|
if (!model)
|
|
209
153
|
return { fits: true, availableBytes: 0, neededBytes: 0, evictionCandidates: [] };
|
|
210
|
-
// Find the currently loaded model for the same category (excluded from eviction)
|
|
211
154
|
const loadedId = this.loadedByCategory.get(model.modality ?? ModelCategory.Language);
|
|
212
155
|
return this.downloader.checkStorageQuota(model, this.metadata, loadedId ?? undefined);
|
|
213
156
|
}
|
|
214
157
|
async downloadModel(modelId) {
|
|
215
158
|
return this.downloader.downloadModel(modelId);
|
|
216
159
|
}
|
|
217
|
-
// --- Model
|
|
160
|
+
// --- Model Import (file picker / drag-drop) ---
|
|
218
161
|
/**
|
|
219
|
-
*
|
|
162
|
+
* Import a model from a user-provided File (via picker or drag-and-drop).
|
|
163
|
+
* Stores the file in the active storage backend and registers it as downloaded.
|
|
164
|
+
* If the model isn't already in the catalog, auto-registers it based on filename.
|
|
220
165
|
*
|
|
221
|
-
* @param
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* Default is `false` — unloads everything to reclaim memory.
|
|
166
|
+
* @param file - The File object from file picker or drag-drop
|
|
167
|
+
* @param modelId - Optional: associate with an existing registered model
|
|
168
|
+
* @returns The model ID (existing or auto-generated)
|
|
225
169
|
*/
|
|
170
|
+
async importModel(file, modelId) {
|
|
171
|
+
let id = modelId ?? sanitizeId(file.name.replace(/\.[^.]+$/, ''));
|
|
172
|
+
// Auto-register if not in the catalog
|
|
173
|
+
if (!this.registry.getModel(id)) {
|
|
174
|
+
const meta = inferModelFromFilename(file.name);
|
|
175
|
+
this.registry.addModel({
|
|
176
|
+
id: meta.id,
|
|
177
|
+
name: meta.name,
|
|
178
|
+
url: '',
|
|
179
|
+
modality: meta.category,
|
|
180
|
+
framework: meta.framework,
|
|
181
|
+
status: ModelStatus.Registered,
|
|
182
|
+
});
|
|
183
|
+
// Use the inferred ID if different
|
|
184
|
+
id = meta.id;
|
|
185
|
+
}
|
|
186
|
+
logger.info(`Importing model from file: ${file.name} (${(file.size / 1024 / 1024).toFixed(1)} MB) -> ${id}`);
|
|
187
|
+
// Stream the file directly to storage to avoid loading the entire file into memory.
|
|
188
|
+
// file.stream() returns a ReadableStream<Uint8Array> in modern browsers.
|
|
189
|
+
if (typeof file.stream === 'function') {
|
|
190
|
+
await this.downloader.storeStreamInOPFS(id, file.stream());
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// Fallback for older browsers: buffer the entire file
|
|
194
|
+
const data = new Uint8Array(await file.arrayBuffer());
|
|
195
|
+
await this.downloader.storeInOPFS(id, data);
|
|
196
|
+
}
|
|
197
|
+
// Use file.size (already known) instead of data.length to avoid extra reference
|
|
198
|
+
const sizeBytes = file.size;
|
|
199
|
+
this.registry.updateModel(id, {
|
|
200
|
+
status: ModelStatus.Downloaded,
|
|
201
|
+
sizeBytes,
|
|
202
|
+
});
|
|
203
|
+
this.touchLastUsed(id, sizeBytes);
|
|
204
|
+
EventBus.shared.emit('model.imported', SDKEventType.Model, {
|
|
205
|
+
modelId: id,
|
|
206
|
+
filename: file.name,
|
|
207
|
+
sizeBytes,
|
|
208
|
+
});
|
|
209
|
+
logger.info(`Model imported: ${id} (${(sizeBytes / 1024 / 1024).toFixed(1)} MB)`);
|
|
210
|
+
return id;
|
|
211
|
+
}
|
|
212
|
+
// --- Model loading orchestration ---
|
|
226
213
|
async loadModel(modelId, options) {
|
|
227
214
|
const model = this.registry.getModel(modelId);
|
|
228
215
|
if (!model || (model.status !== ModelStatus.Downloaded && model.status !== ModelStatus.Registered))
|
|
229
216
|
return false;
|
|
230
217
|
const category = model.modality ?? ModelCategory.Language;
|
|
231
218
|
if (options?.coexist) {
|
|
232
|
-
// Pipeline mode: only unload models of the SAME category (swap).
|
|
233
|
-
// Other categories remain loaded for multi-model workflows.
|
|
234
219
|
const currentId = this.loadedByCategory.get(category);
|
|
235
220
|
if (currentId && currentId !== modelId) {
|
|
236
221
|
logger.info(`Swapping ${category} model: ${currentId} → ${modelId}`);
|
|
@@ -238,61 +223,41 @@ class ModelManagerImpl {
|
|
|
238
223
|
}
|
|
239
224
|
}
|
|
240
225
|
else {
|
|
241
|
-
// Default: Unload ALL currently loaded models before loading the new one.
|
|
242
|
-
//
|
|
243
|
-
// In a browser environment, memory is limited (WASM linear memory +
|
|
244
|
-
// WebGPU buffers). The user interacts with one feature at a time
|
|
245
|
-
// (chat, vision, transcribe, etc.), so there's no need to keep models
|
|
246
|
-
// from other categories resident.
|
|
247
226
|
await this.unloadAll(modelId);
|
|
248
227
|
}
|
|
249
228
|
this.registry.updateModel(modelId, { status: ModelStatus.Loading });
|
|
250
229
|
EventBus.shared.emit('model.loadStarted', SDKEventType.Model, { modelId, category });
|
|
251
230
|
try {
|
|
252
231
|
if (model.modality === ModelCategory.Multimodal) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
if (!
|
|
232
|
+
await this.loadVLMModel(model, modelId);
|
|
233
|
+
}
|
|
234
|
+
else if (model.modality === ModelCategory.SpeechRecognition) {
|
|
235
|
+
const data = await this.downloader.loadFromOPFS(modelId);
|
|
236
|
+
if (!data)
|
|
258
237
|
throw new Error('Model not downloaded — please download the model first.');
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
238
|
+
await this.loadSTTModel(model, data);
|
|
239
|
+
}
|
|
240
|
+
else if (model.modality === ModelCategory.SpeechSynthesis) {
|
|
241
|
+
const data = await this.downloader.loadFromOPFS(modelId);
|
|
242
|
+
if (!data)
|
|
243
|
+
throw new Error('Model not downloaded — please download the model first.');
|
|
244
|
+
await this.loadTTSModel(model, data);
|
|
245
|
+
}
|
|
246
|
+
else if (model.modality === ModelCategory.Audio) {
|
|
247
|
+
const data = await this.downloader.loadFromOPFS(modelId);
|
|
248
|
+
if (!data)
|
|
249
|
+
throw new Error('Model not downloaded — please download the model first.');
|
|
250
|
+
await this.loadVADModel(model, data);
|
|
273
251
|
}
|
|
274
252
|
else {
|
|
275
253
|
const data = await this.downloader.loadFromOPFS(modelId);
|
|
276
|
-
if (!data)
|
|
254
|
+
if (!data)
|
|
277
255
|
throw new Error('Model not downloaded — please download the model first.');
|
|
278
|
-
|
|
279
|
-
if (model.modality === ModelCategory.SpeechRecognition) {
|
|
280
|
-
await this.loadSTTModel(model, data);
|
|
281
|
-
}
|
|
282
|
-
else if (model.modality === ModelCategory.SpeechSynthesis) {
|
|
283
|
-
await this.loadTTSModel(model, data);
|
|
284
|
-
}
|
|
285
|
-
else if (model.modality === ModelCategory.Audio) {
|
|
286
|
-
await this.loadVADModel(model, data);
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
await this.loadLLMModel(model, modelId, data);
|
|
290
|
-
}
|
|
256
|
+
await this.loadLLMModel(model, modelId, data);
|
|
291
257
|
}
|
|
292
258
|
this.loadedByCategory.set(category, modelId);
|
|
293
259
|
this.registry.updateModel(modelId, { status: ModelStatus.Loaded });
|
|
294
260
|
EventBus.shared.emit('model.loadCompleted', SDKEventType.Model, { modelId, category });
|
|
295
|
-
// Update LRU metadata
|
|
296
261
|
this.touchLastUsed(modelId, model.sizeBytes ?? 0);
|
|
297
262
|
return true;
|
|
298
263
|
}
|
|
@@ -313,17 +278,7 @@ class ModelManagerImpl {
|
|
|
313
278
|
const category = model.modality ?? ModelCategory.Language;
|
|
314
279
|
await this.unloadModelByCategory(category);
|
|
315
280
|
}
|
|
316
|
-
/**
|
|
317
|
-
* Unload ALL currently loaded models.
|
|
318
|
-
*
|
|
319
|
-
* Called automatically before loading a new model, and can also be called
|
|
320
|
-
* explicitly by app code (e.g. on tab switch) to release all resources.
|
|
321
|
-
*
|
|
322
|
-
* @param exceptModelId - Optional model ID to skip (the model about to be loaded).
|
|
323
|
-
* Avoids redundant unload+reload of the same model.
|
|
324
|
-
*/
|
|
325
281
|
async unloadAll(exceptModelId) {
|
|
326
|
-
// Snapshot categories to avoid mutation during iteration
|
|
327
282
|
const loaded = [...this.loadedByCategory.entries()];
|
|
328
283
|
if (loaded.length === 0)
|
|
329
284
|
return;
|
|
@@ -335,16 +290,13 @@ class ModelManagerImpl {
|
|
|
335
290
|
}
|
|
336
291
|
}
|
|
337
292
|
async deleteModel(modelId) {
|
|
338
|
-
// Remove from loaded tracking if this model is loaded
|
|
339
293
|
for (const [category, id] of this.loadedByCategory) {
|
|
340
294
|
if (id === modelId) {
|
|
341
295
|
this.loadedByCategory.delete(category);
|
|
342
296
|
break;
|
|
343
297
|
}
|
|
344
298
|
}
|
|
345
|
-
// Delete primary file
|
|
346
299
|
await this.downloader.deleteFromOPFS(modelId);
|
|
347
|
-
// Delete additional files
|
|
348
300
|
const model = this.registry.getModel(modelId);
|
|
349
301
|
if (model?.additionalFiles) {
|
|
350
302
|
for (const file of model.additionalFiles) {
|
|
@@ -354,7 +306,6 @@ class ModelManagerImpl {
|
|
|
354
306
|
this.registry.updateModel(modelId, { status: ModelStatus.Registered, downloadProgress: undefined, sizeBytes: undefined });
|
|
355
307
|
this.removeMetadata(modelId);
|
|
356
308
|
}
|
|
357
|
-
/** Clear all models from OPFS and reset registry statuses. */
|
|
358
309
|
async clearAll() {
|
|
359
310
|
await this.storage.clearAll();
|
|
360
311
|
this.metadata = {};
|
|
@@ -376,10 +327,10 @@ class ModelManagerImpl {
|
|
|
376
327
|
const root = await navigator.storage.getDirectory();
|
|
377
328
|
const modelsDir = await root.getDirectoryHandle('models');
|
|
378
329
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
379
|
-
for await (const
|
|
380
|
-
if (
|
|
330
|
+
for await (const [name, handle] of modelsDir.entries()) {
|
|
331
|
+
if (handle.kind === 'file' && !name.startsWith('_')) {
|
|
381
332
|
modelCount++;
|
|
382
|
-
const file = await
|
|
333
|
+
const file = await handle.getFile();
|
|
383
334
|
totalSize += file.size;
|
|
384
335
|
}
|
|
385
336
|
}
|
|
@@ -398,22 +349,18 @@ class ModelManagerImpl {
|
|
|
398
349
|
return { modelCount, totalSize, available };
|
|
399
350
|
}
|
|
400
351
|
// --- LRU Metadata ---
|
|
401
|
-
/** Get the last-used timestamp for a model (0 if never recorded). */
|
|
402
352
|
getModelLastUsedAt(modelId) {
|
|
403
353
|
return this.metadata[modelId]?.lastUsedAt ?? 0;
|
|
404
354
|
}
|
|
405
|
-
/** Update lastUsedAt for a model and persist to OPFS (fire-and-forget). */
|
|
406
355
|
touchLastUsed(modelId, sizeBytes) {
|
|
407
356
|
this.metadata[modelId] = { lastUsedAt: Date.now(), sizeBytes };
|
|
408
|
-
// Persist asynchronously — don't block the caller
|
|
409
357
|
this.storage.saveMetadata(this.metadata).catch(() => { });
|
|
410
358
|
}
|
|
411
|
-
/** Remove metadata entry when a model is deleted. */
|
|
412
359
|
removeMetadata(modelId) {
|
|
413
360
|
delete this.metadata[modelId];
|
|
414
361
|
this.storage.saveMetadata(this.metadata).catch(() => { });
|
|
415
362
|
}
|
|
416
|
-
// --- Subscriptions
|
|
363
|
+
// --- Subscriptions ---
|
|
417
364
|
onChange(callback) {
|
|
418
365
|
return this.registry.onChange(callback);
|
|
419
366
|
}
|
|
@@ -421,423 +368,130 @@ class ModelManagerImpl {
|
|
|
421
368
|
// Private — model loading by modality
|
|
422
369
|
// ---------------------------------------------------------------------------
|
|
423
370
|
/**
|
|
424
|
-
*
|
|
371
|
+
* Build a ModelLoadContext for passing to backend loaders.
|
|
425
372
|
*/
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
if (!bridge.isLoaded) {
|
|
436
|
-
throw new Error('WASM module not loaded — SDK not initialized.');
|
|
437
|
-
}
|
|
438
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
439
|
-
const m = bridge.module;
|
|
440
|
-
if (typeof m.FS_createPath !== 'function' || typeof m.FS_createDataFile !== 'function') {
|
|
441
|
-
throw new Error('Emscripten FS helper functions not available on WASM module.');
|
|
442
|
-
}
|
|
443
|
-
m.FS_createPath('/', 'models', true, true);
|
|
444
|
-
try {
|
|
445
|
-
m.FS_unlink(fsPath);
|
|
446
|
-
}
|
|
447
|
-
catch { /* File doesn't exist yet */ }
|
|
448
|
-
logger.debug(`Writing ${data.length} bytes to ${fsPath}`);
|
|
449
|
-
m.FS_createDataFile('/models', `${modelId}.gguf`, data, true, true, true);
|
|
450
|
-
logger.debug(`Model file written to ${fsPath}`);
|
|
451
|
-
}
|
|
452
|
-
if (model.modality === ModelCategory.Multimodal) {
|
|
453
|
-
const mmprojFile = model.additionalFiles?.find((f) => f.filename.includes('mmproj'));
|
|
454
|
-
if (!mmprojFile) {
|
|
455
|
-
logger.warning(`No mmproj found, loading as text-only LLM: ${modelId}`);
|
|
456
|
-
if (!this.llmLoader)
|
|
457
|
-
throw new Error('No LLM loader registered. Call ModelManager.setLLMLoader() first.');
|
|
458
|
-
await this.llmLoader.loadModel(fsPath, modelId, model.name);
|
|
459
|
-
}
|
|
460
|
-
else {
|
|
461
|
-
// Ensure mmproj is in OPFS or memory cache (fallback download if missing)
|
|
462
|
-
const mmprojKey = this.downloader.additionalFileKey(modelId, mmprojFile.filename);
|
|
463
|
-
const mmprojExists = await this.downloader.existsInOPFS(mmprojKey);
|
|
464
|
-
if (!mmprojExists && mmprojFile.url) {
|
|
465
|
-
logger.debug(`mmproj not in OPFS, downloading on-demand: ${mmprojFile.filename}`);
|
|
466
|
-
const mmprojDownload = await this.downloader.downloadFile(mmprojFile.url);
|
|
467
|
-
await this.downloader.storeInOPFS(mmprojKey, mmprojDownload);
|
|
468
|
-
}
|
|
469
|
-
if (!this.vlmLoader) {
|
|
470
|
-
throw new Error('No VLM loader registered. Call ModelManager.setVLMLoader() first.');
|
|
471
|
-
}
|
|
472
|
-
// Initialize the Worker (loads its own WASM instance)
|
|
473
|
-
if (!this.vlmLoader.isInitialized) {
|
|
474
|
-
logger.info('Initializing VLM loader...');
|
|
475
|
-
await this.vlmLoader.init();
|
|
476
|
-
}
|
|
477
|
-
// When model/mmproj are only in memory cache (OPFS quota exceeded),
|
|
478
|
-
// we need to read and transfer the data to the Worker.
|
|
479
|
-
let modelDataBuf;
|
|
480
|
-
let mmprojDataBuf;
|
|
481
|
-
const modelInOPFS = await this.downloader.existsInActualOPFS(modelId);
|
|
482
|
-
if (!modelInOPFS && data.length > 0) {
|
|
483
|
-
// data was already read from memory cache in the caller
|
|
484
|
-
modelDataBuf = new ArrayBuffer(data.byteLength);
|
|
485
|
-
new Uint8Array(modelDataBuf).set(data);
|
|
486
|
-
logger.debug(`Transferring model data to VLM Worker (${(data.length / 1024 / 1024).toFixed(1)} MB)`);
|
|
487
|
-
}
|
|
488
|
-
const mmprojInOPFS = await this.downloader.existsInActualOPFS(mmprojKey);
|
|
489
|
-
if (!mmprojInOPFS) {
|
|
490
|
-
const mmprojBytes = await this.downloader.loadFromOPFS(mmprojKey);
|
|
491
|
-
if (mmprojBytes) {
|
|
492
|
-
mmprojDataBuf = new ArrayBuffer(mmprojBytes.byteLength);
|
|
493
|
-
new Uint8Array(mmprojDataBuf).set(mmprojBytes);
|
|
494
|
-
logger.debug(`Transferring mmproj data to VLM Worker (${(mmprojBytes.length / 1024 / 1024).toFixed(1)} MB)`);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
// Load model via the pluggable VLM loader
|
|
498
|
-
logger.info(`Loading VLM model: ${modelId}`);
|
|
499
|
-
await this.vlmLoader.loadModel({
|
|
500
|
-
modelOpfsKey: modelId,
|
|
501
|
-
modelFilename: `${modelId}.gguf`,
|
|
502
|
-
mmprojOpfsKey: mmprojKey,
|
|
503
|
-
mmprojFilename: mmprojFile.filename,
|
|
504
|
-
modelId,
|
|
505
|
-
modelName: model.name,
|
|
506
|
-
modelData: modelDataBuf,
|
|
507
|
-
mmprojData: mmprojDataBuf,
|
|
508
|
-
});
|
|
509
|
-
logger.info(`VLM model loaded: ${modelId}`);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
else if (model.modality === ModelCategory.Language) {
|
|
513
|
-
if (!this.llmLoader)
|
|
514
|
-
throw new Error('No LLM loader registered. Call ModelManager.setLLMLoader() first.');
|
|
515
|
-
await this.llmLoader.loadModel(fsPath, modelId, model.name);
|
|
516
|
-
logger.info(`LLM model loaded via TextGeneration: ${modelId}`);
|
|
517
|
-
}
|
|
373
|
+
buildLoadContext(model, data) {
|
|
374
|
+
return {
|
|
375
|
+
model,
|
|
376
|
+
data,
|
|
377
|
+
downloadFile: (url) => this.downloader.downloadFile(url),
|
|
378
|
+
loadFile: (fileKey) => this.downloader.loadFromOPFS(fileKey),
|
|
379
|
+
storeFile: (fileKey, fileData) => this.downloader.storeInOPFS(fileKey, fileData),
|
|
380
|
+
additionalFileKey: (modelId, filename) => this.downloader.additionalFileKey(modelId, filename),
|
|
381
|
+
};
|
|
518
382
|
}
|
|
519
383
|
/**
|
|
520
|
-
* Load an
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
* 1. **Archive** (isArchive=true): Download is a .tar.gz that bundles encoder,
|
|
524
|
-
* decoder, tokens, etc. Matches the Swift SDK approach.
|
|
525
|
-
* 2. **Individual files**: Separate encoder/decoder/tokens downloads.
|
|
384
|
+
* Load an LLM model via the pluggable loader.
|
|
385
|
+
* The loader (in @runanywhere/web-llamacpp) handles writing to its own
|
|
386
|
+
* Emscripten FS and calling the C API.
|
|
526
387
|
*/
|
|
527
|
-
async
|
|
528
|
-
if (!this.
|
|
529
|
-
throw new Error('No
|
|
530
|
-
const
|
|
531
|
-
await
|
|
532
|
-
|
|
533
|
-
if (model.isArchive) {
|
|
534
|
-
await this.loadSTTFromArchive(model, primaryData, sherpa, modelDir);
|
|
535
|
-
}
|
|
536
|
-
else {
|
|
537
|
-
await this.loadSTTFromIndividualFiles(model, primaryData, sherpa, modelDir);
|
|
538
|
-
}
|
|
539
|
-
logger.info(`STT model loaded via sherpa-onnx: ${model.id}`);
|
|
388
|
+
async loadLLMModel(model, _modelId, data) {
|
|
389
|
+
if (!this.llmLoader)
|
|
390
|
+
throw new Error('No LLM loader registered. Register the @runanywhere/web-llamacpp package.');
|
|
391
|
+
const ctx = this.buildLoadContext(model, data);
|
|
392
|
+
await this.llmLoader.loadModelFromData(ctx);
|
|
393
|
+
logger.info(`LLM model loaded: ${model.id}`);
|
|
540
394
|
}
|
|
541
395
|
/**
|
|
542
|
-
* Load
|
|
543
|
-
* Extracts encoder, decoder, and tokens from the archive automatically.
|
|
396
|
+
* Load a VLM (vision-language) model via the pluggable VLM loader.
|
|
544
397
|
*/
|
|
545
|
-
async
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
sherpa.writeFile(fsPath, entry.data);
|
|
560
|
-
// Auto-discover by filename pattern
|
|
561
|
-
if (relativePath.includes('encoder') && relativePath.endsWith('.onnx')) {
|
|
562
|
-
encoderPath = fsPath;
|
|
563
|
-
}
|
|
564
|
-
else if (relativePath.includes('decoder') && relativePath.endsWith('.onnx')) {
|
|
565
|
-
decoderPath = fsPath;
|
|
566
|
-
}
|
|
567
|
-
else if (relativePath.includes('joiner') && relativePath.endsWith('.onnx')) {
|
|
568
|
-
joinerPath = fsPath;
|
|
569
|
-
}
|
|
570
|
-
else if (relativePath.includes('tokens') && relativePath.endsWith('.txt')) {
|
|
571
|
-
tokensPath = fsPath;
|
|
572
|
-
}
|
|
573
|
-
else if (relativePath.endsWith('.onnx') && !relativePath.includes('encoder') && !relativePath.includes('decoder') && !relativePath.includes('joiner')) {
|
|
574
|
-
modelPath = fsPath;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
// Route to the appropriate STT model type
|
|
578
|
-
if (model.id.includes('whisper')) {
|
|
579
|
-
if (!encoderPath || !decoderPath || !tokensPath) {
|
|
580
|
-
throw new Error(`Whisper archive for '${model.id}' missing encoder/decoder/tokens`);
|
|
581
|
-
}
|
|
582
|
-
await this.sttLoader.loadModel({
|
|
583
|
-
modelId: model.id,
|
|
584
|
-
type: STTModelType.Whisper,
|
|
585
|
-
modelFiles: { encoder: encoderPath, decoder: decoderPath, tokens: tokensPath },
|
|
586
|
-
sampleRate: 16000,
|
|
587
|
-
language: model.language ?? 'en',
|
|
588
|
-
task: model.sttTask ?? 'transcribe',
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
else if (model.id.includes('paraformer')) {
|
|
592
|
-
if (!modelPath || !tokensPath) {
|
|
593
|
-
throw new Error(`Paraformer archive for '${model.id}' missing model/tokens`);
|
|
594
|
-
}
|
|
595
|
-
await this.sttLoader.loadModel({
|
|
596
|
-
modelId: model.id,
|
|
597
|
-
type: STTModelType.Paraformer,
|
|
598
|
-
modelFiles: { model: modelPath, tokens: tokensPath },
|
|
599
|
-
sampleRate: 16000,
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
else if (model.id.includes('zipformer')) {
|
|
603
|
-
if (!encoderPath || !decoderPath || !joinerPath || !tokensPath) {
|
|
604
|
-
throw new Error(`Zipformer archive for '${model.id}' missing encoder/decoder/joiner/tokens`);
|
|
605
|
-
}
|
|
606
|
-
await this.sttLoader.loadModel({
|
|
607
|
-
modelId: model.id,
|
|
608
|
-
type: STTModelType.Zipformer,
|
|
609
|
-
modelFiles: { encoder: encoderPath, decoder: decoderPath, joiner: joinerPath, tokens: tokensPath },
|
|
610
|
-
sampleRate: 16000,
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
else {
|
|
614
|
-
throw new Error(`Unknown STT model type for model: ${model.id}`);
|
|
398
|
+
async loadVLMModel(model, modelId) {
|
|
399
|
+
const exists = await this.downloader.existsInOPFS(modelId);
|
|
400
|
+
if (!exists) {
|
|
401
|
+
throw new Error('Model not downloaded — please download the model first.');
|
|
402
|
+
}
|
|
403
|
+
const mmprojFile = model.additionalFiles?.find((f) => f.filename.includes('mmproj'));
|
|
404
|
+
if (!mmprojFile) {
|
|
405
|
+
// No mmproj — load as text-only LLM
|
|
406
|
+
logger.warning(`No mmproj found, loading as text-only LLM: ${modelId}`);
|
|
407
|
+
const data = await this.downloader.loadFromOPFS(modelId);
|
|
408
|
+
if (!data)
|
|
409
|
+
throw new Error('Model not downloaded.');
|
|
410
|
+
await this.loadLLMModel(model, modelId, data);
|
|
411
|
+
return;
|
|
615
412
|
}
|
|
413
|
+
// Ensure mmproj is available
|
|
414
|
+
const mmprojKey = this.downloader.additionalFileKey(modelId, mmprojFile.filename);
|
|
415
|
+
const mmprojExists = await this.downloader.existsInOPFS(mmprojKey);
|
|
416
|
+
if (!mmprojExists && mmprojFile.url) {
|
|
417
|
+
logger.debug(`mmproj not in storage, downloading on-demand: ${mmprojFile.filename}`);
|
|
418
|
+
const mmprojDownload = await this.downloader.downloadFile(mmprojFile.url);
|
|
419
|
+
await this.downloader.storeInOPFS(mmprojKey, mmprojDownload);
|
|
420
|
+
}
|
|
421
|
+
if (!this.vlmLoader) {
|
|
422
|
+
throw new Error('No VLM loader registered. Call ModelManager.setVLMLoader() first.');
|
|
423
|
+
}
|
|
424
|
+
if (!this.vlmLoader.isInitialized) {
|
|
425
|
+
logger.info('Initializing VLM loader...');
|
|
426
|
+
await this.vlmLoader.init();
|
|
427
|
+
}
|
|
428
|
+
// Transfer data to Worker when model is only in memory cache
|
|
429
|
+
let modelDataBuf;
|
|
430
|
+
let mmprojDataBuf;
|
|
431
|
+
const modelInOPFS = await this.downloader.existsInActualOPFS(modelId);
|
|
432
|
+
if (!modelInOPFS) {
|
|
433
|
+
const data = await this.downloader.loadFromOPFS(modelId);
|
|
434
|
+
if (data && data.length > 0) {
|
|
435
|
+
modelDataBuf = new ArrayBuffer(data.byteLength);
|
|
436
|
+
new Uint8Array(modelDataBuf).set(data);
|
|
437
|
+
logger.debug(`Transferring model data to VLM Worker (${(data.length / 1024 / 1024).toFixed(1)} MB)`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const mmprojInOPFS = await this.downloader.existsInActualOPFS(mmprojKey);
|
|
441
|
+
if (!mmprojInOPFS) {
|
|
442
|
+
const mmprojBytes = await this.downloader.loadFromOPFS(mmprojKey);
|
|
443
|
+
if (mmprojBytes) {
|
|
444
|
+
mmprojDataBuf = new ArrayBuffer(mmprojBytes.byteLength);
|
|
445
|
+
new Uint8Array(mmprojDataBuf).set(mmprojBytes);
|
|
446
|
+
logger.debug(`Transferring mmproj data to VLM Worker (${(mmprojBytes.length / 1024 / 1024).toFixed(1)} MB)`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
logger.info(`Loading VLM model: ${modelId}`);
|
|
450
|
+
await this.vlmLoader.loadModel({
|
|
451
|
+
modelOpfsKey: modelId,
|
|
452
|
+
modelFilename: `${modelId}.gguf`,
|
|
453
|
+
mmprojOpfsKey: mmprojKey,
|
|
454
|
+
mmprojFilename: mmprojFile.filename,
|
|
455
|
+
modelId,
|
|
456
|
+
modelName: model.name,
|
|
457
|
+
modelData: modelDataBuf,
|
|
458
|
+
mmprojData: mmprojDataBuf,
|
|
459
|
+
});
|
|
460
|
+
logger.info(`VLM model loaded: ${modelId}`);
|
|
616
461
|
}
|
|
617
462
|
/**
|
|
618
|
-
* Load an STT model
|
|
463
|
+
* Load an STT model via the pluggable loader.
|
|
464
|
+
* All sherpa-onnx FS operations are handled by the loader.
|
|
619
465
|
*/
|
|
620
|
-
async
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const additionalPaths = {};
|
|
627
|
-
if (model.additionalFiles) {
|
|
628
|
-
for (const file of model.additionalFiles) {
|
|
629
|
-
const fileKey = this.downloader.additionalFileKey(model.id, file.filename);
|
|
630
|
-
let fileData = await this.downloader.loadFromOPFS(fileKey);
|
|
631
|
-
if (!fileData) {
|
|
632
|
-
logger.debug(`Additional file ${file.filename} not in OPFS, downloading...`);
|
|
633
|
-
fileData = await this.downloader.downloadFile(file.url);
|
|
634
|
-
await this.downloader.storeInOPFS(fileKey, fileData);
|
|
635
|
-
}
|
|
636
|
-
const filePath = `${modelDir}/${file.filename}`;
|
|
637
|
-
logger.debug(`Writing STT file to ${filePath} (${fileData.length} bytes)`);
|
|
638
|
-
sherpa.writeFile(filePath, fileData);
|
|
639
|
-
additionalPaths[file.filename] = filePath;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
// Determine model type and build config based on the model ID
|
|
643
|
-
if (model.id.includes('whisper')) {
|
|
644
|
-
const encoderPath = primaryPath;
|
|
645
|
-
const decoderFilename = model.additionalFiles?.find(f => f.filename.includes('decoder'))?.filename;
|
|
646
|
-
const tokensFilename = model.additionalFiles?.find(f => f.filename.includes('tokens'))?.filename;
|
|
647
|
-
if (!decoderFilename || !tokensFilename) {
|
|
648
|
-
throw new Error('Whisper model requires encoder, decoder, and tokens files');
|
|
649
|
-
}
|
|
650
|
-
await this.sttLoader.loadModel({
|
|
651
|
-
modelId: model.id,
|
|
652
|
-
type: STTModelType.Whisper,
|
|
653
|
-
modelFiles: {
|
|
654
|
-
encoder: encoderPath,
|
|
655
|
-
decoder: `${modelDir}/${decoderFilename}`,
|
|
656
|
-
tokens: `${modelDir}/${tokensFilename}`,
|
|
657
|
-
},
|
|
658
|
-
sampleRate: 16000,
|
|
659
|
-
language: model.language ?? 'en',
|
|
660
|
-
task: model.sttTask ?? 'transcribe',
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
else if (model.id.includes('paraformer')) {
|
|
664
|
-
const tokensFilename = model.additionalFiles?.find(f => f.filename.includes('tokens'))?.filename;
|
|
665
|
-
if (!tokensFilename) {
|
|
666
|
-
throw new Error('Paraformer model requires model and tokens files');
|
|
667
|
-
}
|
|
668
|
-
await this.sttLoader.loadModel({
|
|
669
|
-
modelId: model.id,
|
|
670
|
-
type: STTModelType.Paraformer,
|
|
671
|
-
modelFiles: { model: primaryPath, tokens: `${modelDir}/${tokensFilename}` },
|
|
672
|
-
sampleRate: 16000,
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
else if (model.id.includes('zipformer')) {
|
|
676
|
-
const decoderFilename = model.additionalFiles?.find(f => f.filename.includes('decoder'))?.filename;
|
|
677
|
-
const joinerFilename = model.additionalFiles?.find(f => f.filename.includes('joiner'))?.filename;
|
|
678
|
-
const tokensFilename = model.additionalFiles?.find(f => f.filename.includes('tokens'))?.filename;
|
|
679
|
-
if (!decoderFilename || !joinerFilename || !tokensFilename) {
|
|
680
|
-
throw new Error('Zipformer model requires encoder, decoder, joiner, and tokens files');
|
|
681
|
-
}
|
|
682
|
-
await this.sttLoader.loadModel({
|
|
683
|
-
modelId: model.id,
|
|
684
|
-
type: STTModelType.Zipformer,
|
|
685
|
-
modelFiles: {
|
|
686
|
-
encoder: primaryPath,
|
|
687
|
-
decoder: `${modelDir}/${decoderFilename}`,
|
|
688
|
-
joiner: `${modelDir}/${joinerFilename}`,
|
|
689
|
-
tokens: `${modelDir}/${tokensFilename}`,
|
|
690
|
-
},
|
|
691
|
-
sampleRate: 16000,
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
else {
|
|
695
|
-
throw new Error(`Unknown STT model type for model: ${model.id}`);
|
|
696
|
-
}
|
|
466
|
+
async loadSTTModel(model, data) {
|
|
467
|
+
if (!this.sttLoader)
|
|
468
|
+
throw new Error('No STT loader registered. Register the @runanywhere/web-onnx package.');
|
|
469
|
+
const ctx = this.buildLoadContext(model, data);
|
|
470
|
+
await this.sttLoader.loadModelFromData(ctx);
|
|
471
|
+
logger.info(`STT model loaded: ${model.id}`);
|
|
697
472
|
}
|
|
698
473
|
/**
|
|
699
|
-
* Load a TTS model
|
|
700
|
-
*
|
|
701
|
-
* Supports two modes:
|
|
702
|
-
* 1. **Archive** (isArchive=true): Download is a .tar.gz that bundles model files +
|
|
703
|
-
* espeak-ng-data. Matches the Swift SDK approach — extract and write all files.
|
|
704
|
-
* 2. **Individual files** (legacy): Separate model + companion file downloads.
|
|
474
|
+
* Load a TTS model via the pluggable loader.
|
|
475
|
+
* All sherpa-onnx FS operations are handled by the loader.
|
|
705
476
|
*/
|
|
706
|
-
async loadTTSModel(model,
|
|
477
|
+
async loadTTSModel(model, data) {
|
|
707
478
|
if (!this.ttsLoader)
|
|
708
|
-
throw new Error('No TTS loader registered.
|
|
709
|
-
const
|
|
710
|
-
await
|
|
711
|
-
|
|
712
|
-
if (model.isArchive) {
|
|
713
|
-
await this.loadTTSFromArchive(model, primaryData, sherpa, modelDir);
|
|
714
|
-
}
|
|
715
|
-
else {
|
|
716
|
-
await this.loadTTSFromIndividualFiles(model, primaryData, sherpa, modelDir);
|
|
717
|
-
}
|
|
718
|
-
logger.info(`TTS model loaded via sherpa-onnx: ${model.id}`);
|
|
479
|
+
throw new Error('No TTS loader registered. Register the @runanywhere/web-onnx package.');
|
|
480
|
+
const ctx = this.buildLoadContext(model, data);
|
|
481
|
+
await this.ttsLoader.loadModelFromData(ctx);
|
|
482
|
+
logger.info(`TTS model loaded: ${model.id}`);
|
|
719
483
|
}
|
|
720
484
|
/**
|
|
721
|
-
* Load a
|
|
722
|
-
*
|
|
723
|
-
* The archive contains all necessary files in a nested directory:
|
|
724
|
-
* model.onnx, tokens.txt, espeak-ng-data/, etc.
|
|
725
|
-
* We extract everything and write it to the sherpa virtual FS.
|
|
726
|
-
*/
|
|
727
|
-
async loadTTSFromArchive(model, archiveData, sherpa, modelDir) {
|
|
728
|
-
logger.debug(`Extracting TTS archive for ${model.id} (${archiveData.length} bytes)...`);
|
|
729
|
-
const entries = await extractTarGz(archiveData);
|
|
730
|
-
logger.debug(`Extracted ${entries.length} files from archive`);
|
|
731
|
-
// Find the common prefix (nested directory) — archives typically contain
|
|
732
|
-
// one top-level directory with all files inside it.
|
|
733
|
-
const prefix = this.findArchivePrefix(entries.map(e => e.path));
|
|
734
|
-
// Write all extracted files to the sherpa virtual FS
|
|
735
|
-
let modelPath = null;
|
|
736
|
-
let tokensPath = null;
|
|
737
|
-
let dataDirPath = null;
|
|
738
|
-
for (const entry of entries) {
|
|
739
|
-
// Strip the nested directory prefix to get relative path
|
|
740
|
-
const relativePath = prefix ? entry.path.slice(prefix.length) : entry.path;
|
|
741
|
-
const fsPath = `${modelDir}/${relativePath}`;
|
|
742
|
-
sherpa.writeFile(fsPath, entry.data);
|
|
743
|
-
// Auto-discover key files
|
|
744
|
-
if (relativePath.endsWith('.onnx') && !relativePath.includes('/')) {
|
|
745
|
-
modelPath = fsPath;
|
|
746
|
-
}
|
|
747
|
-
if (relativePath === 'tokens.txt') {
|
|
748
|
-
tokensPath = fsPath;
|
|
749
|
-
}
|
|
750
|
-
if (relativePath.startsWith('espeak-ng-data/') && !dataDirPath) {
|
|
751
|
-
dataDirPath = `${modelDir}/espeak-ng-data`;
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
if (!modelPath) {
|
|
755
|
-
throw new Error(`TTS archive for '${model.id}' does not contain an .onnx model file`);
|
|
756
|
-
}
|
|
757
|
-
if (!tokensPath) {
|
|
758
|
-
throw new Error(`TTS archive for '${model.id}' does not contain tokens.txt`);
|
|
759
|
-
}
|
|
760
|
-
logger.debug(`TTS archive extracted — model: ${modelPath}, tokens: ${tokensPath}, dataDir: ${dataDirPath ?? 'none'}`);
|
|
761
|
-
await this.ttsLoader.loadVoice({
|
|
762
|
-
voiceId: model.id,
|
|
763
|
-
modelPath,
|
|
764
|
-
tokensPath,
|
|
765
|
-
dataDir: dataDirPath ?? '',
|
|
766
|
-
numThreads: 1,
|
|
767
|
-
});
|
|
768
|
-
}
|
|
769
|
-
/**
|
|
770
|
-
* Load a TTS model from individual downloaded files.
|
|
771
|
-
* Used when models are registered with individual file URLs (e.g. HuggingFace)
|
|
772
|
-
* rather than tar.gz archives. Downloads espeak-ng-data on-demand for Piper models.
|
|
773
|
-
*/
|
|
774
|
-
async loadTTSFromIndividualFiles(model, primaryData, sherpa, modelDir) {
|
|
775
|
-
const primaryFilename = model.url.split('/').pop();
|
|
776
|
-
const primaryPath = `${modelDir}/${primaryFilename}`;
|
|
777
|
-
logger.debug(`Writing TTS primary file to ${primaryPath} (${primaryData.length} bytes)`);
|
|
778
|
-
sherpa.writeFile(primaryPath, primaryData);
|
|
779
|
-
// Write additional files (tokens.txt, *.json, etc.)
|
|
780
|
-
const additionalPaths = {};
|
|
781
|
-
if (model.additionalFiles) {
|
|
782
|
-
for (const file of model.additionalFiles) {
|
|
783
|
-
const fileKey = this.downloader.additionalFileKey(model.id, file.filename);
|
|
784
|
-
let fileData = await this.downloader.loadFromOPFS(fileKey);
|
|
785
|
-
if (!fileData) {
|
|
786
|
-
logger.debug(`Additional file ${file.filename} not in OPFS, downloading...`);
|
|
787
|
-
fileData = await this.downloader.downloadFile(file.url);
|
|
788
|
-
await this.downloader.storeInOPFS(fileKey, fileData);
|
|
789
|
-
}
|
|
790
|
-
const filePath = `${modelDir}/${file.filename}`;
|
|
791
|
-
logger.debug(`Writing TTS file to ${filePath} (${fileData.length} bytes)`);
|
|
792
|
-
sherpa.writeFile(filePath, fileData);
|
|
793
|
-
additionalPaths[file.filename] = filePath;
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
const tokensPath = additionalPaths['tokens.txt'];
|
|
797
|
-
if (!tokensPath) {
|
|
798
|
-
throw new Error('TTS model requires tokens.txt file');
|
|
799
|
-
}
|
|
800
|
-
await this.ttsLoader.loadVoice({
|
|
801
|
-
voiceId: model.id,
|
|
802
|
-
modelPath: primaryPath,
|
|
803
|
-
tokensPath,
|
|
804
|
-
dataDir: '', // espeak-ng-data is bundled in archives; individual-file path doesn't include it
|
|
805
|
-
numThreads: 1,
|
|
806
|
-
});
|
|
807
|
-
}
|
|
808
|
-
/**
|
|
809
|
-
* Load a VAD model (Silero) into the sherpa-onnx Emscripten FS.
|
|
810
|
-
* The Silero VAD is a single ONNX file — write it to FS and initialise.
|
|
485
|
+
* Load a VAD model via the pluggable loader.
|
|
486
|
+
* All sherpa-onnx FS operations are handled by the loader.
|
|
811
487
|
*/
|
|
812
488
|
async loadVADModel(model, data) {
|
|
813
|
-
const sherpa = SherpaONNXBridge.shared;
|
|
814
|
-
await sherpa.ensureLoaded();
|
|
815
|
-
const modelDir = `/models/${model.id}`;
|
|
816
|
-
const filename = model.url?.split('/').pop() ?? 'silero_vad.onnx';
|
|
817
|
-
const fsPath = `${modelDir}/${filename}`;
|
|
818
|
-
logger.debug(`Writing VAD model to ${fsPath} (${data.length} bytes)`);
|
|
819
|
-
sherpa.writeFile(fsPath, data);
|
|
820
489
|
if (!this.vadLoader)
|
|
821
|
-
throw new Error('No VAD loader registered.
|
|
822
|
-
|
|
490
|
+
throw new Error('No VAD loader registered. Register the @runanywhere/web-onnx package.');
|
|
491
|
+
const ctx = this.buildLoadContext(model, data);
|
|
492
|
+
await this.vadLoader.loadModelFromData(ctx);
|
|
823
493
|
logger.info(`VAD model loaded: ${model.id}`);
|
|
824
494
|
}
|
|
825
|
-
/**
|
|
826
|
-
* Find the common directory prefix in archive entry paths.
|
|
827
|
-
* Archives typically contain a single top-level directory (nested structure).
|
|
828
|
-
* Returns the prefix including trailing '/', or empty string if no common prefix.
|
|
829
|
-
*/
|
|
830
|
-
findArchivePrefix(paths) {
|
|
831
|
-
if (paths.length === 0)
|
|
832
|
-
return '';
|
|
833
|
-
// Check if all paths share a common first directory component
|
|
834
|
-
const firstSlash = paths[0].indexOf('/');
|
|
835
|
-
if (firstSlash === -1)
|
|
836
|
-
return '';
|
|
837
|
-
const candidate = paths[0].slice(0, firstSlash + 1);
|
|
838
|
-
const allMatch = paths.every(p => p.startsWith(candidate));
|
|
839
|
-
return allMatch ? candidate : '';
|
|
840
|
-
}
|
|
841
495
|
/** Unload the currently loaded model for a specific category */
|
|
842
496
|
async unloadModelByCategory(category) {
|
|
843
497
|
const modelId = this.loadedByCategory.get(category);
|
|
@@ -858,27 +512,12 @@ class ModelManagerImpl {
|
|
|
858
512
|
await this.vlmLoader?.unloadModel();
|
|
859
513
|
}
|
|
860
514
|
else {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
// LLM models (Language) write .gguf files into the main-thread
|
|
865
|
-
// Emscripten FS. VLM (Multimodal) models are handled by the Worker's
|
|
866
|
-
// own WASM FS and don't need cleanup here.
|
|
867
|
-
if (category === ModelCategory.Language) {
|
|
868
|
-
try {
|
|
869
|
-
const bridge = WASMBridge.shared;
|
|
870
|
-
if (bridge.isLoaded) {
|
|
871
|
-
const m = bridge.module;
|
|
872
|
-
const fsPath = `/models/${modelId}.gguf`;
|
|
873
|
-
try {
|
|
874
|
-
m.FS_unlink(fsPath);
|
|
875
|
-
}
|
|
876
|
-
catch { /* file may not exist */ }
|
|
877
|
-
logger.debug(`Cleaned up Emscripten FS: ${fsPath}`);
|
|
878
|
-
}
|
|
515
|
+
// LLM: delegate unload + FS cleanup to the backend loader
|
|
516
|
+
if (this.llmLoader?.unloadAndCleanup) {
|
|
517
|
+
await this.llmLoader.unloadAndCleanup(modelId);
|
|
879
518
|
}
|
|
880
|
-
|
|
881
|
-
|
|
519
|
+
else {
|
|
520
|
+
await this.llmLoader?.unloadModel();
|
|
882
521
|
}
|
|
883
522
|
}
|
|
884
523
|
}
|