@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.
Files changed (189) hide show
  1. package/dist/Foundation/ErrorTypes.d.ts +1 -0
  2. package/dist/Foundation/ErrorTypes.d.ts.map +1 -1
  3. package/dist/Foundation/ErrorTypes.js +3 -0
  4. package/dist/Foundation/ErrorTypes.js.map +1 -1
  5. package/dist/Foundation/EventBus.d.ts +0 -1
  6. package/dist/Foundation/EventBus.d.ts.map +1 -1
  7. package/dist/Foundation/StructOffsets.d.ts +5 -37
  8. package/dist/Foundation/StructOffsets.d.ts.map +1 -1
  9. package/dist/Foundation/StructOffsets.js +6 -157
  10. package/dist/Foundation/StructOffsets.js.map +1 -1
  11. package/dist/Foundation/WASMBridge.d.ts +8 -236
  12. package/dist/Foundation/WASMBridge.d.ts.map +1 -1
  13. package/dist/Foundation/WASMBridge.js +7 -388
  14. package/dist/Foundation/WASMBridge.js.map +1 -1
  15. package/dist/Infrastructure/DeviceCapabilities.d.ts.map +1 -1
  16. package/dist/Infrastructure/DeviceCapabilities.js +1 -3
  17. package/dist/Infrastructure/DeviceCapabilities.js.map +1 -1
  18. package/dist/Infrastructure/ExtensionPoint.d.ts +114 -0
  19. package/dist/Infrastructure/ExtensionPoint.d.ts.map +1 -0
  20. package/dist/Infrastructure/ExtensionPoint.js +178 -0
  21. package/dist/Infrastructure/ExtensionPoint.js.map +1 -0
  22. package/dist/Infrastructure/LocalFileStorage.d.ts +134 -0
  23. package/dist/Infrastructure/LocalFileStorage.d.ts.map +1 -0
  24. package/dist/Infrastructure/LocalFileStorage.js +428 -0
  25. package/dist/Infrastructure/LocalFileStorage.js.map +1 -0
  26. package/dist/Infrastructure/ModelDownloader.d.ts +21 -5
  27. package/dist/Infrastructure/ModelDownloader.d.ts.map +1 -1
  28. package/dist/Infrastructure/ModelDownloader.js +79 -7
  29. package/dist/Infrastructure/ModelDownloader.js.map +1 -1
  30. package/dist/Infrastructure/ModelFileInference.d.ts +39 -0
  31. package/dist/Infrastructure/ModelFileInference.d.ts.map +1 -0
  32. package/dist/Infrastructure/ModelFileInference.js +119 -0
  33. package/dist/Infrastructure/ModelFileInference.js.map +1 -0
  34. package/dist/Infrastructure/ModelLoaderTypes.d.ts +91 -12
  35. package/dist/Infrastructure/ModelLoaderTypes.d.ts.map +1 -1
  36. package/dist/Infrastructure/ModelLoaderTypes.js +7 -1
  37. package/dist/Infrastructure/ModelLoaderTypes.js.map +1 -1
  38. package/dist/Infrastructure/ModelManager.d.ts +31 -104
  39. package/dist/Infrastructure/ModelManager.d.ts.map +1 -1
  40. package/dist/Infrastructure/ModelManager.js +207 -568
  41. package/dist/Infrastructure/ModelManager.js.map +1 -1
  42. package/dist/Infrastructure/ModelRegistry.d.ts +6 -8
  43. package/dist/Infrastructure/ModelRegistry.d.ts.map +1 -1
  44. package/dist/Infrastructure/ModelRegistry.js +11 -4
  45. package/dist/Infrastructure/ModelRegistry.js.map +1 -1
  46. package/dist/Infrastructure/OPFSStorage.d.ts +8 -0
  47. package/dist/Infrastructure/OPFSStorage.d.ts.map +1 -1
  48. package/dist/Infrastructure/OPFSStorage.js +37 -0
  49. package/dist/Infrastructure/OPFSStorage.js.map +1 -1
  50. package/dist/Public/Extensions/RunAnywhere+ModelManagement.d.ts +12 -4
  51. package/dist/Public/Extensions/RunAnywhere+ModelManagement.d.ts.map +1 -1
  52. package/dist/Public/Extensions/RunAnywhere+ModelManagement.js +23 -51
  53. package/dist/Public/Extensions/RunAnywhere+ModelManagement.js.map +1 -1
  54. package/dist/Public/Extensions/RunAnywhere+VoiceAgent.d.ts +42 -10
  55. package/dist/Public/Extensions/RunAnywhere+VoiceAgent.d.ts.map +1 -1
  56. package/dist/Public/Extensions/RunAnywhere+VoiceAgent.js +63 -161
  57. package/dist/Public/Extensions/RunAnywhere+VoiceAgent.js.map +1 -1
  58. package/dist/Public/Extensions/RunAnywhere+VoicePipeline.d.ts +3 -29
  59. package/dist/Public/Extensions/RunAnywhere+VoicePipeline.d.ts.map +1 -1
  60. package/dist/Public/Extensions/RunAnywhere+VoicePipeline.js +26 -42
  61. package/dist/Public/Extensions/RunAnywhere+VoicePipeline.js.map +1 -1
  62. package/dist/Public/Extensions/VoicePipelineTypes.d.ts +28 -37
  63. package/dist/Public/Extensions/VoicePipelineTypes.d.ts.map +1 -1
  64. package/dist/Public/Extensions/VoicePipelineTypes.js +4 -1
  65. package/dist/Public/Extensions/VoicePipelineTypes.js.map +1 -1
  66. package/dist/Public/RunAnywhere.d.ts +29 -85
  67. package/dist/Public/RunAnywhere.d.ts.map +1 -1
  68. package/dist/Public/RunAnywhere.js +169 -211
  69. package/dist/Public/RunAnywhere.js.map +1 -1
  70. package/dist/index.d.ts +19 -39
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +15 -31
  73. package/dist/index.js.map +1 -1
  74. package/dist/types/index.d.ts +0 -1
  75. package/dist/types/index.d.ts.map +1 -1
  76. package/dist/types/index.js +1 -0
  77. package/dist/types/index.js.map +1 -1
  78. package/package.json +4 -10
  79. package/dist/Foundation/PlatformAdapter.d.ts +0 -101
  80. package/dist/Foundation/PlatformAdapter.d.ts.map +0 -1
  81. package/dist/Foundation/PlatformAdapter.js +0 -417
  82. package/dist/Foundation/PlatformAdapter.js.map +0 -1
  83. package/dist/Foundation/SherpaONNXBridge.d.ts +0 -147
  84. package/dist/Foundation/SherpaONNXBridge.d.ts.map +0 -1
  85. package/dist/Foundation/SherpaONNXBridge.js +0 -345
  86. package/dist/Foundation/SherpaONNXBridge.js.map +0 -1
  87. package/dist/Infrastructure/AudioCapture.d.ts +0 -99
  88. package/dist/Infrastructure/AudioCapture.d.ts.map +0 -1
  89. package/dist/Infrastructure/AudioCapture.js +0 -264
  90. package/dist/Infrastructure/AudioCapture.js.map +0 -1
  91. package/dist/Infrastructure/AudioPlayback.d.ts +0 -53
  92. package/dist/Infrastructure/AudioPlayback.d.ts.map +0 -1
  93. package/dist/Infrastructure/AudioPlayback.js +0 -117
  94. package/dist/Infrastructure/AudioPlayback.js.map +0 -1
  95. package/dist/Infrastructure/VLMWorkerBridge.d.ts +0 -211
  96. package/dist/Infrastructure/VLMWorkerBridge.d.ts.map +0 -1
  97. package/dist/Infrastructure/VLMWorkerBridge.js +0 -264
  98. package/dist/Infrastructure/VLMWorkerBridge.js.map +0 -1
  99. package/dist/Infrastructure/VLMWorkerRuntime.d.ts +0 -38
  100. package/dist/Infrastructure/VLMWorkerRuntime.d.ts.map +0 -1
  101. package/dist/Infrastructure/VLMWorkerRuntime.js +0 -503
  102. package/dist/Infrastructure/VLMWorkerRuntime.js.map +0 -1
  103. package/dist/Infrastructure/VideoCapture.d.ts +0 -130
  104. package/dist/Infrastructure/VideoCapture.d.ts.map +0 -1
  105. package/dist/Infrastructure/VideoCapture.js +0 -236
  106. package/dist/Infrastructure/VideoCapture.js.map +0 -1
  107. package/dist/Public/Extensions/DiffusionTypes.d.ts +0 -64
  108. package/dist/Public/Extensions/DiffusionTypes.d.ts.map +0 -1
  109. package/dist/Public/Extensions/DiffusionTypes.js +0 -28
  110. package/dist/Public/Extensions/DiffusionTypes.js.map +0 -1
  111. package/dist/Public/Extensions/EmbeddingsTypes.d.ts +0 -33
  112. package/dist/Public/Extensions/EmbeddingsTypes.d.ts.map +0 -1
  113. package/dist/Public/Extensions/EmbeddingsTypes.js +0 -13
  114. package/dist/Public/Extensions/EmbeddingsTypes.js.map +0 -1
  115. package/dist/Public/Extensions/RunAnywhere+Diffusion.d.ts +0 -44
  116. package/dist/Public/Extensions/RunAnywhere+Diffusion.d.ts.map +0 -1
  117. package/dist/Public/Extensions/RunAnywhere+Diffusion.js +0 -189
  118. package/dist/Public/Extensions/RunAnywhere+Diffusion.js.map +0 -1
  119. package/dist/Public/Extensions/RunAnywhere+Embeddings.d.ts +0 -56
  120. package/dist/Public/Extensions/RunAnywhere+Embeddings.d.ts.map +0 -1
  121. package/dist/Public/Extensions/RunAnywhere+Embeddings.js +0 -240
  122. package/dist/Public/Extensions/RunAnywhere+Embeddings.js.map +0 -1
  123. package/dist/Public/Extensions/RunAnywhere+STT.d.ts +0 -97
  124. package/dist/Public/Extensions/RunAnywhere+STT.d.ts.map +0 -1
  125. package/dist/Public/Extensions/RunAnywhere+STT.js +0 -417
  126. package/dist/Public/Extensions/RunAnywhere+STT.js.map +0 -1
  127. package/dist/Public/Extensions/RunAnywhere+StructuredOutput.d.ts +0 -69
  128. package/dist/Public/Extensions/RunAnywhere+StructuredOutput.d.ts.map +0 -1
  129. package/dist/Public/Extensions/RunAnywhere+StructuredOutput.js +0 -196
  130. package/dist/Public/Extensions/RunAnywhere+StructuredOutput.js.map +0 -1
  131. package/dist/Public/Extensions/RunAnywhere+TTS.d.ts +0 -55
  132. package/dist/Public/Extensions/RunAnywhere+TTS.d.ts.map +0 -1
  133. package/dist/Public/Extensions/RunAnywhere+TTS.js +0 -253
  134. package/dist/Public/Extensions/RunAnywhere+TTS.js.map +0 -1
  135. package/dist/Public/Extensions/RunAnywhere+TextGeneration.d.ts +0 -80
  136. package/dist/Public/Extensions/RunAnywhere+TextGeneration.d.ts.map +0 -1
  137. package/dist/Public/Extensions/RunAnywhere+TextGeneration.js +0 -470
  138. package/dist/Public/Extensions/RunAnywhere+TextGeneration.js.map +0 -1
  139. package/dist/Public/Extensions/RunAnywhere+ToolCalling.d.ts +0 -82
  140. package/dist/Public/Extensions/RunAnywhere+ToolCalling.d.ts.map +0 -1
  141. package/dist/Public/Extensions/RunAnywhere+ToolCalling.js +0 -576
  142. package/dist/Public/Extensions/RunAnywhere+ToolCalling.js.map +0 -1
  143. package/dist/Public/Extensions/RunAnywhere+VAD.d.ts +0 -70
  144. package/dist/Public/Extensions/RunAnywhere+VAD.d.ts.map +0 -1
  145. package/dist/Public/Extensions/RunAnywhere+VAD.js +0 -231
  146. package/dist/Public/Extensions/RunAnywhere+VAD.js.map +0 -1
  147. package/dist/Public/Extensions/RunAnywhere+VLM.d.ts +0 -58
  148. package/dist/Public/Extensions/RunAnywhere+VLM.d.ts.map +0 -1
  149. package/dist/Public/Extensions/RunAnywhere+VLM.js +0 -262
  150. package/dist/Public/Extensions/RunAnywhere+VLM.js.map +0 -1
  151. package/dist/Public/Extensions/STTTypes.d.ts +0 -61
  152. package/dist/Public/Extensions/STTTypes.d.ts.map +0 -1
  153. package/dist/Public/Extensions/STTTypes.js +0 -16
  154. package/dist/Public/Extensions/STTTypes.js.map +0 -1
  155. package/dist/Public/Extensions/TTSTypes.d.ts +0 -31
  156. package/dist/Public/Extensions/TTSTypes.d.ts.map +0 -1
  157. package/dist/Public/Extensions/TTSTypes.js +0 -3
  158. package/dist/Public/Extensions/TTSTypes.js.map +0 -1
  159. package/dist/Public/Extensions/ToolCallingTypes.d.ts +0 -78
  160. package/dist/Public/Extensions/ToolCallingTypes.d.ts.map +0 -1
  161. package/dist/Public/Extensions/ToolCallingTypes.js +0 -8
  162. package/dist/Public/Extensions/ToolCallingTypes.js.map +0 -1
  163. package/dist/Public/Extensions/VADTypes.d.ts +0 -30
  164. package/dist/Public/Extensions/VADTypes.d.ts.map +0 -1
  165. package/dist/Public/Extensions/VADTypes.js +0 -8
  166. package/dist/Public/Extensions/VADTypes.js.map +0 -1
  167. package/dist/Public/Extensions/VLMTypes.d.ts +0 -56
  168. package/dist/Public/Extensions/VLMTypes.d.ts.map +0 -1
  169. package/dist/Public/Extensions/VLMTypes.js +0 -24
  170. package/dist/Public/Extensions/VLMTypes.js.map +0 -1
  171. package/dist/types/LLMTypes.d.ts +0 -48
  172. package/dist/types/LLMTypes.d.ts.map +0 -1
  173. package/dist/types/LLMTypes.js +0 -8
  174. package/dist/types/LLMTypes.js.map +0 -1
  175. package/dist/workers/vlm-worker.d.ts +0 -9
  176. package/dist/workers/vlm-worker.d.ts.map +0 -1
  177. package/dist/workers/vlm-worker.js +0 -10
  178. package/dist/workers/vlm-worker.js.map +0 -1
  179. package/wasm/racommons-webgpu.js +0 -156
  180. package/wasm/racommons-webgpu.wasm +0 -0
  181. package/wasm/racommons.js +0 -126
  182. package/wasm/racommons.wasm +0 -0
  183. package/wasm/sherpa/sherpa-onnx-asr.js +0 -1538
  184. package/wasm/sherpa/sherpa-onnx-glue-original.js +0 -19
  185. package/wasm/sherpa/sherpa-onnx-glue.js +0 -17
  186. package/wasm/sherpa/sherpa-onnx-tts.js +0 -657
  187. package/wasm/sherpa/sherpa-onnx-vad.js +0 -337
  188. package/wasm/sherpa/sherpa-onnx-wave.js +0 -88
  189. 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
- * The public API is unchanged `ModelManager` is still a singleton that
8
- * exposes `registerModels()`, `downloadModel()`, `loadModel()`, `onChange()`, etc.
9
- * Internally it delegates catalog operations to the Registry and download
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 { extractTarGz } from './ArchiveUtility';
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 the Public layer during init */
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 (called by the app) ---
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 — non-critical
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
- // Load persisted metadata (lastUsedAt timestamps)
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 (delegated to registry) ---
136
- getModels() {
137
- return this.registry.getModels();
138
- }
139
- getModelsByCategory(category) {
140
- return this.registry.getModelsByCategory(category);
141
- }
142
- getModelsByFramework(framework) {
143
- return this.registry.getModelsByFramework(framework);
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 (delegated to downloader) ---
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 loading orchestration ---
160
+ // --- Model Import (file picker / drag-drop) ---
218
161
  /**
219
- * Load a model by ID.
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 options.coexist When `true`, only unload the model of the **same
222
- * category** (swap) rather than unloading ALL loaded models. Use this for
223
- * multi-model pipelines like Voice (STT + LLM + TTS).
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
- // VLM: Worker reads from OPFS directly when possible.
254
- // When OPFS quota is exceeded, models live only in the main-thread
255
- // memory cache we must read and transfer them to the Worker.
256
- const exists = await this.downloader.existsInOPFS(modelId);
257
- if (!exists) {
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
- const inActualOPFS = await this.downloader.existsInActualOPFS(modelId);
261
- if (inActualOPFS) {
262
- // Worker can read from OPFS directly (optimal: avoids main-thread copy)
263
- await this.loadLLMModel(model, modelId, new Uint8Array(0));
264
- }
265
- else {
266
- // Model is in memory cache only (OPFS quota exceeded) — read and transfer to Worker
267
- logger.debug(`VLM model ${modelId} not in OPFS, reading from memory cache to transfer to Worker`);
268
- const data = await this.downloader.loadFromOPFS(modelId);
269
- if (!data)
270
- throw new Error('Model not downloaded — please download the model first.');
271
- await this.loadLLMModel(model, modelId, data);
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 entry of modelsDir.values()) {
380
- if (entry.kind === 'file') {
330
+ for await (const [name, handle] of modelsDir.entries()) {
331
+ if (handle.kind === 'file' && !name.startsWith('_')) {
381
332
  modelCount++;
382
- const file = await entry.getFile();
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 (delegated to registry) ---
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
- * Load an LLM/VLM model into the RACommons Emscripten FS.
371
+ * Build a ModelLoadContext for passing to backend loaders.
425
372
  */
426
- async loadLLMModel(model, modelId, data) {
427
- const fsDir = `/models`;
428
- const fsPath = `${fsDir}/${modelId}.gguf`;
429
- if (model.modality === ModelCategory.Multimodal) {
430
- // VLM models are loaded in a dedicated Web Worker that reads from OPFS.
431
- }
432
- else {
433
- // Text-only LLM: write to main-thread Emscripten FS as before
434
- const bridge = WASMBridge.shared;
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 STT model into sherpa-onnx.
521
- *
522
- * Supports two modes:
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 loadSTTModel(model, primaryData) {
528
- if (!this.sttLoader)
529
- throw new Error('No STT loader registered. Call ModelManager.setSTTLoader() first.');
530
- const sherpa = SherpaONNXBridge.shared;
531
- await sherpa.ensureLoaded();
532
- const modelDir = `/models/${model.id}`;
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 an STT model from a .tar.gz archive (matching Swift SDK approach).
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 loadSTTFromArchive(model, archiveData, sherpa, modelDir) {
546
- logger.debug(`Extracting STT archive for ${model.id} (${archiveData.length} bytes)...`);
547
- const entries = await extractTarGz(archiveData);
548
- logger.debug(`Extracted ${entries.length} files from STT archive`);
549
- const prefix = this.findArchivePrefix(entries.map(e => e.path));
550
- // Write all files and auto-discover key paths
551
- let encoderPath = null;
552
- let decoderPath = null;
553
- let tokensPath = null;
554
- let joinerPath = null;
555
- let modelPath = null;
556
- for (const entry of entries) {
557
- const relativePath = prefix ? entry.path.slice(prefix.length) : entry.path;
558
- const fsPath = `${modelDir}/${relativePath}`;
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 from individual downloaded files (legacy path).
463
+ * Load an STT model via the pluggable loader.
464
+ * All sherpa-onnx FS operations are handled by the loader.
619
465
  */
620
- async loadSTTFromIndividualFiles(model, primaryData, sherpa, modelDir) {
621
- const primaryFilename = model.url.split('/').pop();
622
- const primaryPath = `${modelDir}/${primaryFilename}`;
623
- logger.debug(`Writing STT primary file to ${primaryPath} (${primaryData.length} bytes)`);
624
- sherpa.writeFile(primaryPath, primaryData);
625
- // Write additional files to sherpa FS (download on-demand if missing from OPFS)
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 into the sherpa-onnx Emscripten FS and initialise the TTS engine.
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, primaryData) {
477
+ async loadTTSModel(model, data) {
707
478
  if (!this.ttsLoader)
708
- throw new Error('No TTS loader registered. Call ModelManager.setTTSLoader() first.');
709
- const sherpa = SherpaONNXBridge.shared;
710
- await sherpa.ensureLoaded();
711
- const modelDir = `/models/${model.id}`;
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 TTS model from a .tar.gz archive (matching Swift SDK approach).
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. Call ModelManager.setVADLoader() first.');
822
- await this.vadLoader.loadModel({ modelPath: fsPath });
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
- await this.llmLoader?.unloadModel();
862
- }
863
- // Clean up Emscripten FS model files to release WASM linear memory.
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
- catch {
881
- // Non-critical — FS cleanup is best-effort
519
+ else {
520
+ await this.llmLoader?.unloadModel();
882
521
  }
883
522
  }
884
523
  }