shubeydoo commited on
Commit
accf76b
·
1 Parent(s): 6840ff4

Liquid AI LFM2-VL-450M-WebGPU Demo

Browse files
Files changed (16) hide show
  1. .gitignore +3 -0
  2. Dockerfile +37 -0
  3. README.md +26 -5
  4. assets/liquid-ai.svg +1 -0
  5. config.js +54 -0
  6. index.html +780 -0
  7. infer.js +79 -0
  8. main.js +214 -0
  9. package-lock.json +1994 -0
  10. package.json +18 -0
  11. styles.css +1103 -0
  12. ui.js +273 -0
  13. vite.config.js +27 -0
  14. vl-model.js +974 -0
  15. vl-processor.js +497 -0
  16. webgpu-inference.js +192 -0
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ node_modules/
2
+ dist/
3
+ .claude/
Dockerfile ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Build stage
2
+ FROM node:20-alpine AS builder
3
+
4
+ WORKDIR /app
5
+ COPY package*.json ./
6
+ RUN npm install
7
+ COPY . .
8
+ RUN npm run build
9
+
10
+ # Production stage
11
+ FROM nginx:alpine
12
+
13
+ # Copy files
14
+ COPY --from=builder /app/dist /usr/share/nginx/html/
15
+
16
+ # Custom nginx config with COEP/COOP/CORP headers
17
+ RUN echo 'server { \
18
+ listen 7860; \
19
+ location / { \
20
+ root /usr/share/nginx/html; \
21
+ index index.html; \
22
+ try_files $uri $uri/ /index.html; \
23
+ add_header Cross-Origin-Opener-Policy same-origin; \
24
+ add_header Cross-Origin-Embedder-Policy require-corp; \
25
+ add_header Cross-Origin-Resource-Policy cross-origin; \
26
+ add_header Cache-Control "no-cache, no-store, must-revalidate"; \
27
+ } \
28
+ location ~* \.(js|css|wasm)$ { \
29
+ root /usr/share/nginx/html; \
30
+ add_header Cross-Origin-Opener-Policy same-origin; \
31
+ add_header Cross-Origin-Embedder-Policy require-corp; \
32
+ add_header Cross-Origin-Resource-Policy cross-origin; \
33
+ add_header Cache-Control "no-cache, must-revalidate"; \
34
+ } \
35
+ }' > /etc/nginx/conf.d/default.conf
36
+
37
+ EXPOSE 7860
README.md CHANGED
@@ -1,10 +1,31 @@
1
  ---
2
- title: LFM2 VL 450M WebGPU
3
- emoji: 🐨
4
- colorFrom: gray
5
- colorTo: gray
6
  sdk: docker
7
  pinned: false
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: LFM2-VL-450M WebGPU
3
+ emoji: ⚡️
4
+ colorFrom: purple
5
+ colorTo: blue
6
  sdk: docker
7
  pinned: false
8
+ models:
9
+ - onnx-community/LFM2-VL-450M-ONNX
10
+ - onnx-community/LFM2-VL-450M
11
+ short_description: In-browser vision-language inference with LFM2-VL-450M
12
  ---
13
 
14
+ # LFM2-VL-450M WebGPU Demo
15
+
16
+ In-browser vision-language inference with LFM2-VL-450M, powered by ONNX Runtime and WebGPU.
17
+
18
+ Everything runs entirely in your browser with WebGPU acceleration - no data is sent to a server.
19
+
20
+ ## Features
21
+
22
+ - **Live Webcam Captioning**: Stream from your webcam with real-time AI-generated captions
23
+ - **Multiple Precision Options**: Choose between FP16 (~1.05 GB) or FP32 (~2.1 GB)
24
+ - **Browser Caching**: Models are cached locally after first download for faster subsequent loads
25
+ - **Adjustable Resolution**: Configure capture resolution (256-512px) for performance tuning
26
+
27
+ ## Requirements
28
+
29
+ - WebGPU-enabled browser
30
+ - ~1-2 GB memory depending on precision choice
31
+ - Webcam access for live captioning
assets/liquid-ai.svg ADDED
config.js ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Configuration for LFM2-VL-450M Demo
3
+ * WebGPU inference with ONNX models from HuggingFace Hub
4
+ */
5
+
6
+ const HF_BASE = 'https://huggingface.co/onnx-community/LFM2-VL-450M-ONNX/resolve/main';
7
+
8
+ // Model configurations
9
+ export const MODELS = {
10
+ 'LFM2-VL-450M-FP16': {
11
+ id: 'LFM2-VL-450M-FP16',
12
+ path: HF_BASE,
13
+ label: 'FP16 (Half Precision)',
14
+ size: '~1.05 GB',
15
+ quantization: { decoder: 'fp16', visionEncoder: 'fp16' }
16
+ },
17
+ 'LFM2-VL-450M-FP32': {
18
+ id: 'LFM2-VL-450M-FP32',
19
+ path: HF_BASE,
20
+ label: 'FP32 (Full Precision)',
21
+ size: '~2.1 GB',
22
+ quantization: { decoder: null, visionEncoder: null }
23
+ }
24
+ };
25
+
26
+ // Default settings
27
+ export const DEFAULT_CONFIG = {
28
+ defaultModel: 'LFM2-VL-450M-FP16',
29
+ maxNewTokens: 512,
30
+ temperature: 0.0
31
+ };
32
+
33
+ // Get config with optional env overrides
34
+ export function getConfig() {
35
+ const config = { ...DEFAULT_CONFIG };
36
+
37
+ if (typeof import.meta !== 'undefined' && import.meta.env) {
38
+ if (import.meta.env.VITE_DEFAULT_MODEL) {
39
+ config.defaultModel = import.meta.env.VITE_DEFAULT_MODEL;
40
+ }
41
+ }
42
+
43
+ return config;
44
+ }
45
+
46
+ // Get model configuration by ID
47
+ export function getModelConfig(modelId) {
48
+ return MODELS[modelId];
49
+ }
50
+
51
+ // Get all available models
52
+ export function getAvailableModels() {
53
+ return Object.values(MODELS);
54
+ }
index.html ADDED
@@ -0,0 +1,780 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>LFM2-VL-450M Demo</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="styles.css">
11
+ </head>
12
+ <body>
13
+ <!-- Loading Screen -->
14
+ <div id="loading-screen" class="loading-screen">
15
+ <!-- Animated Background Canvas -->
16
+ <canvas id="loading-canvas" class="loading-canvas"></canvas>
17
+
18
+ <!-- Vignette Overlay -->
19
+ <div class="loading-vignette"></div>
20
+
21
+ <!-- Main Content -->
22
+ <div class="loading-content">
23
+ <div class="loading-header" style="display: flex; justify-content: center; align-items: center;">
24
+ <img
25
+ src="assets/liquid-ai.svg"
26
+ alt="Liquid AI"
27
+ class="loading-logo"
28
+ style="
29
+ width: min(16vw, 128px);
30
+ height: min(16vw, 128px);
31
+ max-width: 40vw;
32
+ max-height: 40vw;
33
+ min-width: 64px;
34
+ min-height: 64px;
35
+ object-fit: contain;
36
+ transition: width 0.2s, height 0.2s;
37
+ ">
38
+ </div>
39
+
40
+ <div class="loading-title-section">
41
+ <h1 class="loading-title">LFM2-VL-450M WebGPU</h1>
42
+ <p class="loading-subtitle">Vision-Language Model in Your Browser</p>
43
+ </div>
44
+
45
+ <div class="loading-description">
46
+ <p>This demo showcases in-browser vision-language inference with LFM2-VL-450M, powered by ONNX Runtime and WebGPU.</p>
47
+ <p>Everything runs entirely in your browser with WebGPU acceleration, meaning no data is sent to a server.</p>
48
+ </div>
49
+
50
+ <div class="loading-action-section">
51
+ <button id="loading-explore-btn" class="loading-explore-button">
52
+ <span id="loading-btn-text">Explore</span>
53
+ <span id="loading-spinner" class="loading-spinner hidden"></span>
54
+ <span id="loading-progress-text" class="loading-progress-text hidden"></span>
55
+ </button>
56
+ </div>
57
+
58
+ <div id="loading-error" class="loading-error hidden">
59
+ <p id="loading-error-text"></p>
60
+ <button id="loading-retry-btn" class="loading-retry-button">Retry</button>
61
+ </div>
62
+ </div>
63
+ </div>
64
+
65
+ <div class="app-layout">
66
+ <!-- Top Navigation -->
67
+ <div class="top-nav">
68
+ <div class="nav-left">
69
+ <img src="assets/liquid-ai.svg" alt="Liquid AI" class="nav-logo-img">
70
+ <a href="https://www.liquid.ai" target="_blank" rel="noopener noreferrer" class="nav-logo-link">Liquid</a>
71
+ </div>
72
+ <div class="nav-center">
73
+ <span class="nav-title">LFM2-VL-450M</span>
74
+ <span class="nav-subtitle">Stream from your webcam with real-time captions,powered by your own hardware with WebGPU!</span>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- Main Content Area -->
79
+ <div class="container">
80
+ <!-- Live Caption Mode -->
81
+ <div id="live-caption-mode" class="mode-container active">
82
+ <div class="live-caption-content">
83
+ <div class="live-caption-video-section">
84
+ <div class="live-caption-video-container">
85
+ <video id="live-caption-video" autoplay></video>
86
+ <!-- Capture Overlay (on video) -->
87
+ <div class="capture-overlay">
88
+ <button id="start-live-caption-btn" class="control-btn primary">Start</button>
89
+ <div class="overlay-field">
90
+ <label class="overlay-label">Input:</label>
91
+ <select id="live-caption-resolution-select" class="control-select">
92
+ <option value="256">256px</option>
93
+ <option value="384">384px</option>
94
+ <option value="448">448px</option>
95
+ <option value="512" selected>512px</option>
96
+ </select>
97
+ </div>
98
+ <div class="capture-status">
99
+ <span class="status-indicator" id="live-status-indicator"></span>
100
+ <span class="status-text" id="live-status-text">Idle</span>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ <div class="controls-bar">
105
+ <!-- Status Row -->
106
+ <div class="controls-row status-row">
107
+ <span class="model-status" id="model-status"></span>
108
+ <!-- Progress Bar (shown during loading) -->
109
+ <div class="progress-bar-row" id="loading-progress" style="display: none;">
110
+ <div class="progress-fill" id="progress-fill" style="width: 0%"></div>
111
+ </div>
112
+ </div>
113
+ <!-- Controls Row -->
114
+ <div class="controls-row">
115
+ <div class="control-group model-group">
116
+ <label class="control-label">Select quantization:</label>
117
+ <select id="model-select" class="control-select model-select">
118
+ <!-- Options populated from config.js -->
119
+ </select>
120
+ <button class="control-btn" id="reload-model-btn" title="Load Model">Load</button>
121
+ </div>
122
+ <div class="control-group cache-group">
123
+ <span class="cache-info" id="cache-info">0 MB</span>
124
+ <button class="control-btn small" id="clear-cache-btn" disabled title="Clear Cache">Clear</button>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ <div class="live-caption-text-section">
130
+ <h3 class="caption-section-title">Captions</h3>
131
+ <div class="latest-caption" id="latest-caption">Start capturing to see live captions...</div>
132
+ <div class="caption-history" id="caption-history"></div>
133
+ </div>
134
+ </div>
135
+ </div>
136
+
137
+ </div>
138
+ </div>
139
+
140
+ <script>
141
+ // ============================================
142
+ // LOADING SCREEN
143
+ // ============================================
144
+
145
+ let loadingScreenVisible = true;
146
+ let loadingCanvas = null;
147
+ let loadingCtx = null;
148
+ let loadingDots = [];
149
+ let loadingAnimationId = null;
150
+
151
+ function initLoadingScreen() {
152
+ // Initialize canvas animation
153
+ loadingCanvas = document.getElementById('loading-canvas');
154
+ if (!loadingCanvas) return;
155
+
156
+ loadingCtx = loadingCanvas.getContext('2d');
157
+ setupLoadingCanvas();
158
+ animateLoadingCanvas();
159
+
160
+ // Set up event listeners
161
+ setupLoadingScreenListeners();
162
+
163
+ // Handle window resize
164
+ window.addEventListener('resize', setupLoadingCanvas);
165
+ }
166
+
167
+ function setupLoadingCanvas() {
168
+ if (!loadingCanvas || !loadingCtx) return;
169
+
170
+ loadingCanvas.width = window.innerWidth;
171
+ loadingCanvas.height = window.innerHeight;
172
+
173
+ // Create dots
174
+ loadingDots = [];
175
+ const numDots = Math.floor((loadingCanvas.width * loadingCanvas.height) / 15000);
176
+ for (let i = 0; i < numDots; i++) {
177
+ loadingDots.push({
178
+ x: Math.random() * loadingCanvas.width,
179
+ y: Math.random() * loadingCanvas.height,
180
+ radius: Math.random() * 1.5 + 0.5,
181
+ speed: Math.random() * 0.5 + 0.1,
182
+ opacity: Math.random() * 0.5 + 0.2,
183
+ blur: Math.random() > 0.7 ? Math.random() * 2 + 1 : 0
184
+ });
185
+ }
186
+ }
187
+
188
+ function animateLoadingCanvas() {
189
+ if (!loadingCtx || !loadingCanvas) return;
190
+
191
+ loadingCtx.clearRect(0, 0, loadingCanvas.width, loadingCanvas.height);
192
+
193
+ loadingDots.forEach(dot => {
194
+ // Update position
195
+ dot.y += dot.speed;
196
+ if (dot.y > loadingCanvas.height) {
197
+ dot.y = 0 - dot.radius;
198
+ dot.x = Math.random() * loadingCanvas.width;
199
+ }
200
+
201
+ // Draw dot
202
+ loadingCtx.beginPath();
203
+ loadingCtx.arc(dot.x, dot.y, dot.radius, 0, Math.PI * 2);
204
+ loadingCtx.fillStyle = `rgba(255, 255, 255, ${dot.opacity})`;
205
+ if (dot.blur > 0) {
206
+ loadingCtx.filter = `blur(${dot.blur}px)`;
207
+ }
208
+ loadingCtx.fill();
209
+ loadingCtx.filter = 'none';
210
+ });
211
+
212
+ loadingAnimationId = requestAnimationFrame(animateLoadingCanvas);
213
+ }
214
+
215
+ function setupLoadingScreenListeners() {
216
+ // Explore button
217
+ const exploreBtn = document.getElementById('loading-explore-btn');
218
+ if (exploreBtn) {
219
+ exploreBtn.addEventListener('click', handleLoadingScreenLoad);
220
+ }
221
+
222
+ // Retry button
223
+ const retryBtn = document.getElementById('loading-retry-btn');
224
+ if (retryBtn) {
225
+ retryBtn.addEventListener('click', handleLoadingScreenLoad);
226
+ }
227
+ }
228
+
229
+ async function handleLoadingScreenLoad() {
230
+ const exploreBtn = document.getElementById('loading-explore-btn');
231
+
232
+ if (!exploreBtn) return;
233
+
234
+ // Simply hide the loading screen - user can load model manually via the input field
235
+ hideLoadingScreen();
236
+ }
237
+
238
+ function hideLoadingScreen() {
239
+ const screen = document.getElementById('loading-screen');
240
+ if (screen) {
241
+ screen.classList.add('hidden');
242
+ loadingScreenVisible = false;
243
+
244
+ // Stop canvas animation
245
+ if (loadingAnimationId) {
246
+ cancelAnimationFrame(loadingAnimationId);
247
+ loadingAnimationId = null;
248
+ }
249
+
250
+ // Clear any URL hash from old bookmarks/links
251
+ if (window.location.hash) {
252
+ history.replaceState(null, '', window.location.pathname);
253
+ }
254
+ }
255
+ }
256
+
257
+ function showLoadingScreen() {
258
+ const screen = document.getElementById('loading-screen');
259
+ if (screen) {
260
+ screen.classList.remove('hidden');
261
+ loadingScreenVisible = true;
262
+
263
+ // Restart canvas animation if needed
264
+ if (!loadingAnimationId && loadingCanvas) {
265
+ animateLoadingCanvas();
266
+ }
267
+ }
268
+ }
269
+
270
+ function isMobileDevice() {
271
+ return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
272
+ || (navigator.maxTouchPoints > 0 && window.innerWidth < 1024);
273
+ }
274
+
275
+ function showMobileWarning() {
276
+ if (!isMobileDevice()) return;
277
+
278
+ const warningDiv = document.createElement('div');
279
+ warningDiv.className = 'mobile-warning';
280
+ warningDiv.innerHTML = `
281
+ <div class="mobile-warning-title">
282
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
283
+ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
284
+ <line x1="12" y1="9" x2="12" y2="13"></line>
285
+ <line x1="12" y1="17" x2="12.01" y2="17"></line>
286
+ </svg>
287
+ Mobile Device Detected
288
+ </div>
289
+ <p>This demo requires a desktop browser. Mobile browsers have memory limits smaller than the model size.</p>
290
+ <p>Please visit this page on a desktop computer.</p>
291
+ `;
292
+
293
+ const actionSection = document.querySelector('.loading-action-section');
294
+ if (actionSection) {
295
+ actionSection.parentNode.insertBefore(warningDiv, actionSection);
296
+ }
297
+
298
+ const btnText = document.getElementById('loading-btn-text');
299
+ if (btnText) {
300
+ btnText.textContent = 'Try Anyway';
301
+ }
302
+ }
303
+
304
+ function isSafariDesktop() {
305
+ const ua = navigator.userAgent;
306
+ // Safari has "Safari" in UA but Chrome/Edge also include it
307
+ // True Safari doesn't have "Chrome" or "Chromium" in UA
308
+ const isSafari = /Safari/.test(ua) && !/Chrome|Chromium/.test(ua);
309
+ // Exclude iOS (iPhone, iPad, iPod)
310
+ const isIOS = /iPhone|iPad|iPod/.test(ua);
311
+ return isSafari && !isIOS;
312
+ }
313
+
314
+ function showSafariWarning() {
315
+ if (!isSafariDesktop()) return;
316
+
317
+ // Add Safari class to body for CSS targeting
318
+ document.body.classList.add('is-safari');
319
+
320
+ const warningDiv = document.createElement('div');
321
+ warningDiv.className = 'safari-warning';
322
+ warningDiv.innerHTML = `
323
+ <div class="safari-warning-title">
324
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
325
+ <circle cx="12" cy="12" r="10"></circle>
326
+ <line x1="12" y1="8" x2="12" y2="12"></line>
327
+ <line x1="12" y1="16" x2="12.01" y2="16"></line>
328
+ </svg>
329
+ Safari requires WebGPU to be explicitly enabled
330
+ </div>
331
+ <p>WebGPU must be manually enabled in Safari. Go to <strong>Safari → Settings → Feature Flags </strong> and enable <strong>WebGPU</strong>.</p>
332
+ <p><a href="https://webkit.org/blog/14879/webgpu-now-available-for-testing-in-safari-technology-preview/" target="_blank" rel="noopener noreferrer">Learn more about WebGPU in Safari</a></p>
333
+ `;
334
+
335
+ const actionSection = document.querySelector('.loading-action-section');
336
+ if (actionSection) {
337
+ actionSection.parentNode.insertBefore(warningDiv, actionSection);
338
+ }
339
+ }
340
+
341
+ // Initialize loading screen on page load
342
+ if (document.readyState === 'loading') {
343
+ document.addEventListener('DOMContentLoaded', () => {
344
+ initLoadingScreen();
345
+ showMobileWarning();
346
+ showSafariWarning();
347
+ });
348
+ } else {
349
+ initLoadingScreen();
350
+ showMobileWarning();
351
+ showSafariWarning();
352
+ }
353
+
354
+ // ============================================
355
+ // GENERATION CONFIG
356
+ // ============================================
357
+
358
+ const generationConfig = {
359
+ max_new_tokens: 128
360
+ };
361
+
362
+ function getModeConfig() {
363
+ return {
364
+ max_new_tokens: generationConfig.max_new_tokens
365
+ };
366
+ }
367
+
368
+ // Make getModeConfig available globally for main.js
369
+ window.getModeConfig = getModeConfig;
370
+
371
+ // ============================================
372
+ // LIVE CAPTION MODE
373
+ // ============================================
374
+
375
+ let liveCaptionStream = null;
376
+ let isCapturing = false;
377
+ let captureLoopRunning = false;
378
+
379
+
380
+ async function startLiveCaption() {
381
+ try {
382
+ const video = document.getElementById('live-caption-video');
383
+ const statusIndicator = document.getElementById('live-status-indicator');
384
+ const statusText = document.getElementById('live-status-text');
385
+ const startBtn = document.getElementById('start-live-caption-btn');
386
+
387
+ // Get webcam stream
388
+ liveCaptionStream = await navigator.mediaDevices.getUserMedia({
389
+ video: { width: 1024, height: 1024 }
390
+ });
391
+ video.srcObject = liveCaptionStream;
392
+
393
+ // Wait for video to actually start playing before capturing
394
+ await new Promise((resolve) => {
395
+ video.onloadeddata = () => {
396
+ video.play();
397
+ resolve();
398
+ };
399
+ // If already loaded, resolve immediately
400
+ if (video.readyState >= 2) {
401
+ video.play();
402
+ resolve();
403
+ }
404
+ });
405
+
406
+ // Wait for camera to warm up (avoid black first frame)
407
+ await new Promise(resolve => setTimeout(resolve, 500));
408
+
409
+ // Update UI
410
+ isCapturing = true;
411
+ statusIndicator.classList.add('active');
412
+ statusText.textContent = 'Capturing';
413
+ startBtn.textContent = 'Stop';
414
+ startBtn.classList.add('stop');
415
+
416
+ // Hide the initial placeholder text
417
+ const latestCaption = document.getElementById('latest-caption');
418
+ if (latestCaption) {
419
+ latestCaption.style.display = 'none';
420
+ }
421
+
422
+ // Start capture loop (waits for each generation to complete)
423
+ captureLoop();
424
+
425
+ } catch (error) {
426
+ console.error('Error starting live caption:', error);
427
+ alert('Could not access webcam. Please check permissions.');
428
+ }
429
+ }
430
+
431
+ function stopLiveCaption() {
432
+ const video = document.getElementById('live-caption-video');
433
+ const statusIndicator = document.getElementById('live-status-indicator');
434
+ const statusText = document.getElementById('live-status-text');
435
+ const startBtn = document.getElementById('start-live-caption-btn');
436
+
437
+ // Setting isCapturing to false stops the capture loop
438
+ isCapturing = false;
439
+
440
+ if (liveCaptionStream) {
441
+ liveCaptionStream.getTracks().forEach(track => track.stop());
442
+ liveCaptionStream = null;
443
+ }
444
+
445
+ if (video) {
446
+ video.srcObject = null;
447
+ }
448
+
449
+ if (statusIndicator) statusIndicator.classList.remove('active');
450
+ if (statusText) statusText.textContent = 'Idle';
451
+ if (startBtn) {
452
+ startBtn.textContent = 'Start';
453
+ startBtn.classList.remove('stop');
454
+ }
455
+ }
456
+
457
+ // Expose globally so main.js can stop capture before loading new model
458
+ window.stopLiveCaption = stopLiveCaption;
459
+
460
+ /**
461
+ * Async capture loop - waits for each generation to complete before starting next
462
+ */
463
+ async function captureLoop() {
464
+ if (!isCapturing || captureLoopRunning) return;
465
+ captureLoopRunning = true;
466
+
467
+ while (isCapturing) {
468
+ await captureLiveCaptionFrame();
469
+ }
470
+
471
+ captureLoopRunning = false;
472
+ }
473
+
474
+ async function captureLiveCaptionFrame() {
475
+ if (!isCapturing) return;
476
+
477
+ const video = document.getElementById('live-caption-video');
478
+ const statusText = document.getElementById('live-status-text');
479
+
480
+ // Get resolution from dropdown
481
+ const resolutionSelect = document.getElementById('live-caption-resolution-select');
482
+ const resolution = parseInt(resolutionSelect?.value || '384', 10);
483
+
484
+ // Create canvas and capture frame
485
+ const canvas = document.createElement('canvas');
486
+ canvas.width = resolution;
487
+ canvas.height = resolution;
488
+ const ctx = canvas.getContext('2d');
489
+ ctx.drawImage(video, 0, 0, resolution, resolution);
490
+
491
+ // Use DataURL - browser's native JPEG encoding is faster than JS ImageData handling
492
+ const dataURL = canvas.toDataURL('image/jpeg', 0.8);
493
+
494
+ try {
495
+ const config = getModeConfig();
496
+
497
+ // Wait for webgpuInit to be available
498
+ if (!window.webgpuInit) {
499
+ await new Promise((resolve, reject) => {
500
+ if (window.webgpuInit) {
501
+ resolve();
502
+ } else {
503
+ const timeout = setTimeout(() => {
504
+ reject(new Error('WebGPU system initialization timeout'));
505
+ }, 10000);
506
+ window.addEventListener('webgpu-ready', () => {
507
+ clearTimeout(timeout);
508
+ resolve();
509
+ }, { once: true });
510
+ }
511
+ });
512
+ }
513
+
514
+ if (!window.webgpuInit.isModelLoaded()) {
515
+ if (statusText) statusText.textContent = 'Model not loaded';
516
+ return;
517
+ }
518
+
519
+ // Build message
520
+ const messages = [{
521
+ role: 'user',
522
+ content: [
523
+ { type: 'image', value: dataURL },
524
+ { type: 'text', value: 'Describe what you see in one sentence.' }
525
+ ]
526
+ }];
527
+
528
+ const response = await window.webgpuInit.generate(messages, {
529
+ maxNewTokens: config.max_new_tokens
530
+ });
531
+
532
+ updateLiveCaption(response);
533
+
534
+ } catch (error) {
535
+ console.error('Error generating caption:', error);
536
+ if (statusText) statusText.textContent = 'Error';
537
+ }
538
+ }
539
+
540
+ function updateLiveCaption(caption) {
541
+ // Skip empty or whitespace-only captions (happens when model is busy)
542
+ if (!caption || !caption.trim()) {
543
+ return;
544
+ }
545
+
546
+ const captionHistory = document.getElementById('caption-history');
547
+
548
+ // Remove 'latest' class from all existing items
549
+ const existingItems = captionHistory.querySelectorAll('.caption-history-item');
550
+ existingItems.forEach(item => item.classList.remove('latest'));
551
+
552
+ // Add to history at the top (most recent first)
553
+ const timestamp = new Date().toLocaleTimeString();
554
+ const historyItem = document.createElement('div');
555
+ historyItem.className = 'caption-history-item latest';
556
+ historyItem.innerHTML = `
557
+ <span class="caption-timestamp">${timestamp}</span>
558
+ <span class="caption-text">${caption.trim()}</span>
559
+ `;
560
+ captionHistory.prepend(historyItem);
561
+
562
+ // Keep only the last 7 items, remove oldest from bottom
563
+ while (captionHistory.children.length > 7) {
564
+ captionHistory.removeChild(captionHistory.lastChild);
565
+ }
566
+
567
+ // Apply fading effect: newest (first) is fully visible, older ones fade
568
+ const items = captionHistory.children;
569
+ for (let i = 0; i < items.length; i++) {
570
+ // First item is 1.0, then fade to 0.1 for older items
571
+ const opacity = Math.max(0.1, 1.0 - (i * 0.2));
572
+ items[i].style.opacity = opacity;
573
+ }
574
+ }
575
+
576
+ // Model state storage - don't restore from localStorage since model needs to be reloaded each session
577
+ let CURRENT_MODEL = '';
578
+
579
+ function formatModelName(modelId) {
580
+ // Convert "LFM2-VL-450M-FP16" to "LFM2-VL-450M FP16"
581
+ if (!modelId || modelId === 'Loading...') return modelId;
582
+ // Convert trailing "-FP16" or "-FP32" to " FP16" or " FP32"
583
+ let clean = modelId.replace(/-(FP16|FP32)$/, ' $1');
584
+ return clean;
585
+ }
586
+
587
+ function updateModelStatus(modelId = null) {
588
+ const statusEl = document.getElementById('model-status');
589
+ if (statusEl) {
590
+ if (modelId) {
591
+ CURRENT_MODEL = modelId;
592
+ localStorage.setItem('CURRENT_MODEL', modelId);
593
+ statusEl.textContent = formatModelName(modelId);
594
+ statusEl.style.color = 'var(--text-primary)';
595
+ } else {
596
+ CURRENT_MODEL = '';
597
+ localStorage.removeItem('CURRENT_MODEL');
598
+ statusEl.textContent = 'No model loaded';
599
+ statusEl.style.color = 'var(--text-secondary)';
600
+ }
601
+ }
602
+ }
603
+
604
+ async function loadSelectedModel(modelId) {
605
+ const modelSelect = document.getElementById('model-select');
606
+ const reloadBtn = document.getElementById('reload-model-btn');
607
+
608
+ // Show loading state
609
+ if (modelSelect) {
610
+ modelSelect.disabled = true;
611
+ }
612
+
613
+ if (reloadBtn) {
614
+ reloadBtn.disabled = true;
615
+ reloadBtn.style.opacity = '0.6';
616
+ reloadBtn.style.cursor = 'not-allowed';
617
+ }
618
+
619
+ // Update status to loading
620
+ updateModelStatus('Loading...');
621
+
622
+ try {
623
+ // Wait for webgpuInit to be available
624
+ if (!window.webgpuInit) {
625
+ await new Promise((resolve, reject) => {
626
+ if (window.webgpuInit) {
627
+ resolve();
628
+ } else {
629
+ const timeout = setTimeout(() => {
630
+ reject(new Error('WebGPU system initialization timeout. Please refresh the page.'));
631
+ }, 10000);
632
+ window.addEventListener('webgpu-ready', () => {
633
+ clearTimeout(timeout);
634
+ resolve();
635
+ }, { once: true });
636
+ }
637
+ });
638
+ }
639
+
640
+ await window.webgpuInit.handleLoadModel();
641
+
642
+ // Store current model (status already updated by handleLoadModel)
643
+ CURRENT_MODEL = modelId;
644
+ localStorage.setItem('CURRENT_MODEL', CURRENT_MODEL);
645
+
646
+ // Enable buttons when model loads
647
+ if (window.webgpuInit && window.webgpuInit.updateButtonStates) {
648
+ window.webgpuInit.updateButtonStates(true);
649
+ }
650
+ } catch (error) {
651
+ updateModelStatus(null);
652
+ alert(`Error loading model: ${error.message}`);
653
+ console.error('Error loading model:', error);
654
+ } finally {
655
+ // Restore UI state
656
+ if (modelSelect) {
657
+ modelSelect.disabled = false;
658
+ }
659
+
660
+ if (reloadBtn) {
661
+ reloadBtn.disabled = false;
662
+ reloadBtn.style.opacity = '1';
663
+ reloadBtn.style.cursor = 'pointer';
664
+ }
665
+ }
666
+ }
667
+
668
+ // Inference via WebGPU
669
+ async function generate(messages, options = {}) {
670
+ const config = getModeConfig();
671
+
672
+ // Wait for webgpuInit to be available
673
+ if (!window.webgpuInit) {
674
+ // Wait for webgpu-ready event
675
+ await new Promise((resolve, reject) => {
676
+ if (window.webgpuInit) {
677
+ resolve();
678
+ } else {
679
+ const timeout = setTimeout(() => {
680
+ reject(new Error('WebGPU system initialization timeout. Please refresh the page.'));
681
+ }, 10000);
682
+ window.addEventListener('webgpu-ready', () => {
683
+ clearTimeout(timeout);
684
+ resolve();
685
+ }, { once: true });
686
+ }
687
+ });
688
+ }
689
+
690
+ if (!window.webgpuInit.isModelLoaded()) {
691
+ throw new Error('Model not loaded. Please load a model first.');
692
+ }
693
+
694
+ try {
695
+ // Generate using WebGPU with streaming support
696
+ const response = await window.webgpuInit.generate(messages, {
697
+ maxNewTokens: config.max_new_tokens,
698
+ onToken: options.onToken || ((token) => {
699
+ // Default: do nothing if no callback provided
700
+ return false;
701
+ })
702
+ });
703
+
704
+ return response;
705
+ } catch (error) {
706
+ console.error('Error calling WebGPU inference:', error);
707
+ throw error;
708
+ }
709
+ }
710
+
711
+ // Initialize
712
+ function init() {
713
+ setupModeEventListeners();
714
+
715
+ // Clear any URL hash from old bookmarks/links
716
+ if (window.location.hash) {
717
+ history.replaceState(null, '', window.location.pathname);
718
+ }
719
+ }
720
+
721
+ function setupModeEventListeners() {
722
+ const reloadModelBtn = document.getElementById('reload-model-btn');
723
+ if (reloadModelBtn) {
724
+ reloadModelBtn.addEventListener('click', () => {
725
+ const modelSelect = document.getElementById('model-select');
726
+ const selectedModelId = modelSelect?.value;
727
+ if (!selectedModelId) {
728
+ alert('Please select a model first.');
729
+ return;
730
+ }
731
+ loadSelectedModel(selectedModelId);
732
+ });
733
+ }
734
+
735
+ // Live Caption controls
736
+ const liveCaptionBtn = document.getElementById('start-live-caption-btn');
737
+ if (liveCaptionBtn) {
738
+ liveCaptionBtn.addEventListener('click', () => {
739
+ if (isCapturing) {
740
+ stopLiveCaption();
741
+ } else {
742
+ startLiveCaption();
743
+ }
744
+ });
745
+ }
746
+
747
+ // Model selector dropdown
748
+ const modelSelect = document.getElementById('model-select');
749
+ if (modelSelect) {
750
+ // Restore last selected model if saved
751
+ const savedModelId = localStorage.getItem('SELECTED_MODEL_ID');
752
+ if (savedModelId && modelSelect.querySelector(`option[value="${savedModelId}"]`)) {
753
+ modelSelect.value = savedModelId;
754
+ }
755
+
756
+ // Initialize model status display
757
+ if (CURRENT_MODEL) {
758
+ updateModelStatus(CURRENT_MODEL);
759
+ } else {
760
+ updateModelStatus(null);
761
+ }
762
+
763
+ // Save selection on change
764
+ modelSelect.addEventListener('change', (e) => {
765
+ localStorage.setItem('SELECTED_MODEL_ID', e.target.value);
766
+ });
767
+ }
768
+ }
769
+
770
+ if (document.readyState === 'loading') {
771
+ document.addEventListener('DOMContentLoaded', init);
772
+ } else {
773
+ init();
774
+ }
775
+ </script>
776
+
777
+ <!-- Load main.js module -->
778
+ <script type="module" src="./main.js"></script>
779
+ </body>
780
+ </html>
infer.js ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Inference Router
3
+ * Routes inference requests to WebGPU
4
+ */
5
+
6
+ import { getWebGPUInference, clearModelCache, getCacheInfo } from './webgpu-inference.js';
7
+
8
+ // Re-export cache utilities for UI
9
+ export { clearModelCache, getCacheInfo };
10
+
11
+ // Engine instance (lazy initialized)
12
+ let webgpuEngine = null;
13
+
14
+ /**
15
+ * Load a model
16
+ * @param {string} modelId - Model ID from config
17
+ * @param {object} options - Loading options
18
+ */
19
+ export async function loadModel(modelId, options = {}) {
20
+ if (!webgpuEngine) {
21
+ webgpuEngine = getWebGPUInference();
22
+ }
23
+
24
+ await webgpuEngine.loadModel(modelId, options);
25
+ }
26
+
27
+ /**
28
+ * Check if a model is loaded
29
+ * @returns {boolean}
30
+ */
31
+ export function isModelLoaded() {
32
+ return webgpuEngine && webgpuEngine.isModelLoaded();
33
+ }
34
+
35
+ /**
36
+ * Get current model ID
37
+ * @returns {string|null}
38
+ */
39
+ export function getCurrentModelId() {
40
+ return webgpuEngine ? webgpuEngine.getCurrentModelId() : null;
41
+ }
42
+
43
+ /**
44
+ * Generate a response from messages
45
+ * @param {Array<Object>} messages - Array of message objects with role and content
46
+ * @param {object} options - Generation options
47
+ * @param {function} options.onToken - Token callback for streaming
48
+ * @returns {Promise<string>} Generated response
49
+ */
50
+ export async function generate(messages, options = {}) {
51
+ if (!webgpuEngine) {
52
+ webgpuEngine = getWebGPUInference();
53
+ }
54
+
55
+ if (!webgpuEngine.isModelLoaded()) {
56
+ throw new Error('Model not loaded. Please load a model first.');
57
+ }
58
+
59
+ return await webgpuEngine.generate(messages, options);
60
+ }
61
+
62
+ /**
63
+ * Clear the image embedding cache (call when starting a new conversation)
64
+ */
65
+ export function clearImageCache() {
66
+ if (webgpuEngine) {
67
+ webgpuEngine.clearImageCache();
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Dispose resources
73
+ */
74
+ export function dispose() {
75
+ if (webgpuEngine) {
76
+ webgpuEngine.dispose();
77
+ webgpuEngine = null;
78
+ }
79
+ }
main.js ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Main application bootstrap
3
+ * Initializes the WebGPU model loading and wires up event handlers
4
+ */
5
+
6
+ import {
7
+ setupEventListeners,
8
+ populateModelSelector,
9
+ toggleModelSection,
10
+ updateLoadingProgress,
11
+ showLoadingProgress,
12
+ showModelInputWrapper,
13
+ updateModelStatus,
14
+ updateWebGPUStatus,
15
+ setLoadModelButtonEnabled,
16
+ getSelectedModelId,
17
+ updateButtonStates,
18
+ updateCacheInfo,
19
+ setupClearCacheHandler,
20
+ setClearCacheButtonText
21
+ } from './ui.js';
22
+ import {
23
+ generate,
24
+ loadModel,
25
+ isModelLoaded,
26
+ getCurrentModelId,
27
+ clearImageCache,
28
+ clearModelCache,
29
+ getCacheInfo
30
+ } from './infer.js';
31
+ import { getAvailableModels, getModelConfig } from './config.js';
32
+
33
+ /**
34
+ * Handle loading a model
35
+ */
36
+ async function handleLoadModel() {
37
+ const modelId = getSelectedModelId();
38
+ if (!modelId) {
39
+ updateModelStatus('Please select a model', 'error');
40
+ return;
41
+ }
42
+
43
+ // Stop capturing if active (prevents crash)
44
+ if (window.stopLiveCaption) {
45
+ window.stopLiveCaption();
46
+ }
47
+
48
+ // Clear image cache when loading a new model
49
+ clearImageCache();
50
+
51
+ setLoadModelButtonEnabled(false);
52
+ showModelInputWrapper(false);
53
+ showLoadingProgress(true);
54
+ updateButtonStates(false); // Disable Start button while loading
55
+ updateModelStatus('Loading model, will take a few minutes if not cached...', 'loading');
56
+
57
+ try {
58
+ await loadModel(modelId, {
59
+ progressCallback: (progress) => {
60
+ if (progress.status === 'loading') {
61
+ const percent = Math.round(progress.progress || 0);
62
+ updateLoadingProgress(percent);
63
+ // Show file download progress (includes MB downloaded / total)
64
+ const statusText = progress.file
65
+ ? `Downloading: ${progress.file}`
66
+ : 'Loading model...';
67
+ updateModelStatus(statusText, 'loading');
68
+ } else if (progress.status === 'done') {
69
+ updateLoadingProgress(100);
70
+ }
71
+ }
72
+ });
73
+
74
+ showLoadingProgress(false);
75
+ showModelInputWrapper(true);
76
+ const modelConfig = getModelConfig(modelId);
77
+ const modelLabel = modelConfig ? `LFM2-VL-450M ${modelConfig.label}` : modelId;
78
+ updateModelStatus(`Loaded ${modelLabel}`, 'success');
79
+ updateButtonStates(true);
80
+ await refreshCacheInfo();
81
+ } catch (error) {
82
+ console.error('Model loading error:', error);
83
+ if (error.message && error.message.includes('already loading')) {
84
+ updateModelStatus('Model loading in progress...', 'loading');
85
+ return;
86
+ }
87
+ showLoadingProgress(false);
88
+ showModelInputWrapper(true);
89
+ updateModelStatus(`Error: ${error.message}`, 'error');
90
+ updateButtonStates(false);
91
+ } finally {
92
+ setLoadModelButtonEnabled(true);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Handle reloading the current model
98
+ */
99
+ async function handleReloadModel() {
100
+ const currentModelId = getCurrentModelId();
101
+ if (!currentModelId) {
102
+ updateModelStatus('No model loaded', 'error');
103
+ return;
104
+ }
105
+
106
+ await handleLoadModel();
107
+ }
108
+
109
+ /**
110
+ * Update cache storage info display
111
+ */
112
+ async function refreshCacheInfo() {
113
+ const info = await getCacheInfo();
114
+ updateCacheInfo(info ? info.used : 0);
115
+ }
116
+
117
+ /**
118
+ * Handle clearing the model cache
119
+ */
120
+ async function handleClearCache() {
121
+ const info = await getCacheInfo();
122
+ const usedMB = info ? (info.used / 1024 / 1024).toFixed(0) : 0;
123
+
124
+ const confirmed = confirm(
125
+ `Delete downloaded model files?\n\n` +
126
+ `This will free up ~${usedMB} MB of storage.\n` +
127
+ `Models will be re-downloaded next time you load them.`
128
+ );
129
+ if (!confirmed) return;
130
+
131
+ setClearCacheButtonText('Deleting...');
132
+ await clearModelCache();
133
+ setClearCacheButtonText('Clear');
134
+ await refreshCacheInfo();
135
+ updateModelStatus('Downloaded models deleted', 'success');
136
+ }
137
+
138
+ /**
139
+ * Check WebGPU availability
140
+ */
141
+ async function checkWebGPU() {
142
+ if (!navigator.gpu) {
143
+ updateWebGPUStatus('WebGPU not available. Enable at chrome://flags/#enable-unsafe-webgpu', false);
144
+ return false;
145
+ }
146
+
147
+ try {
148
+ const adapter = await navigator.gpu.requestAdapter();
149
+ if (!adapter) {
150
+ updateWebGPUStatus('WebGPU adapter not found', false);
151
+ return false;
152
+ }
153
+
154
+ const info = adapter.info || {};
155
+ const desc = info.description || info.vendor || info.architecture || 'Available';
156
+ updateWebGPUStatus(`WebGPU: ${desc}`, true);
157
+ return true;
158
+ } catch (error) {
159
+ updateWebGPUStatus(`WebGPU error: ${error.message}`, false);
160
+ return false;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Initialize the application
166
+ */
167
+ async function init() {
168
+ // Populate model selector
169
+ populateModelSelector(getAvailableModels());
170
+
171
+ // Check WebGPU availability
172
+ await checkWebGPU();
173
+
174
+ // Set up event listeners
175
+ setupEventListeners(null, handleLoadModel, handleReloadModel);
176
+
177
+ // Set up cache handler
178
+ setupClearCacheHandler(handleClearCache);
179
+
180
+ // Show model section (WebGPU only)
181
+ toggleModelSection(true);
182
+
183
+ // Initialize button states (disabled until model loads)
184
+ updateButtonStates(false);
185
+
186
+ // Initialize cache info display
187
+ await refreshCacheInfo();
188
+ }
189
+
190
+ // Export functions for use by inline script
191
+ window.webgpuInit = {
192
+ init,
193
+ handleLoadModel,
194
+ handleReloadModel,
195
+ checkWebGPU,
196
+ populateModelSelector: () => populateModelSelector(getAvailableModels()),
197
+ toggleModelSection,
198
+ updateModelStatus,
199
+ getCurrentModelId,
200
+ isModelLoaded,
201
+ getAvailableModels,
202
+ generate,
203
+ updateButtonStates
204
+ };
205
+
206
+ // Signal that WebGPU module is ready
207
+ window.dispatchEvent(new Event('webgpu-ready'));
208
+
209
+ // Initialize when DOM is ready
210
+ if (document.readyState === 'loading') {
211
+ document.addEventListener('DOMContentLoaded', init);
212
+ } else {
213
+ init();
214
+ }
package-lock.json ADDED
@@ -0,0 +1,1994 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "lfm25-vl-webgpu",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "lfm25-vl-webgpu",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "@huggingface/transformers": "^3.7.1",
12
+ "onnxruntime-web": "^1.23.2"
13
+ },
14
+ "devDependencies": {
15
+ "vite": "^5.4.0"
16
+ }
17
+ },
18
+ "node_modules/@emnapi/runtime": {
19
+ "version": "1.8.1",
20
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
21
+ "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
22
+ "license": "MIT",
23
+ "optional": true,
24
+ "dependencies": {
25
+ "tslib": "^2.4.0"
26
+ }
27
+ },
28
+ "node_modules/@esbuild/aix-ppc64": {
29
+ "version": "0.21.5",
30
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
31
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
32
+ "cpu": [
33
+ "ppc64"
34
+ ],
35
+ "dev": true,
36
+ "license": "MIT",
37
+ "optional": true,
38
+ "os": [
39
+ "aix"
40
+ ],
41
+ "engines": {
42
+ "node": ">=12"
43
+ }
44
+ },
45
+ "node_modules/@esbuild/android-arm": {
46
+ "version": "0.21.5",
47
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
48
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
49
+ "cpu": [
50
+ "arm"
51
+ ],
52
+ "dev": true,
53
+ "license": "MIT",
54
+ "optional": true,
55
+ "os": [
56
+ "android"
57
+ ],
58
+ "engines": {
59
+ "node": ">=12"
60
+ }
61
+ },
62
+ "node_modules/@esbuild/android-arm64": {
63
+ "version": "0.21.5",
64
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
65
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
66
+ "cpu": [
67
+ "arm64"
68
+ ],
69
+ "dev": true,
70
+ "license": "MIT",
71
+ "optional": true,
72
+ "os": [
73
+ "android"
74
+ ],
75
+ "engines": {
76
+ "node": ">=12"
77
+ }
78
+ },
79
+ "node_modules/@esbuild/android-x64": {
80
+ "version": "0.21.5",
81
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
82
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
83
+ "cpu": [
84
+ "x64"
85
+ ],
86
+ "dev": true,
87
+ "license": "MIT",
88
+ "optional": true,
89
+ "os": [
90
+ "android"
91
+ ],
92
+ "engines": {
93
+ "node": ">=12"
94
+ }
95
+ },
96
+ "node_modules/@esbuild/darwin-arm64": {
97
+ "version": "0.21.5",
98
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
99
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
100
+ "cpu": [
101
+ "arm64"
102
+ ],
103
+ "dev": true,
104
+ "license": "MIT",
105
+ "optional": true,
106
+ "os": [
107
+ "darwin"
108
+ ],
109
+ "engines": {
110
+ "node": ">=12"
111
+ }
112
+ },
113
+ "node_modules/@esbuild/darwin-x64": {
114
+ "version": "0.21.5",
115
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
116
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
117
+ "cpu": [
118
+ "x64"
119
+ ],
120
+ "dev": true,
121
+ "license": "MIT",
122
+ "optional": true,
123
+ "os": [
124
+ "darwin"
125
+ ],
126
+ "engines": {
127
+ "node": ">=12"
128
+ }
129
+ },
130
+ "node_modules/@esbuild/freebsd-arm64": {
131
+ "version": "0.21.5",
132
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
133
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
134
+ "cpu": [
135
+ "arm64"
136
+ ],
137
+ "dev": true,
138
+ "license": "MIT",
139
+ "optional": true,
140
+ "os": [
141
+ "freebsd"
142
+ ],
143
+ "engines": {
144
+ "node": ">=12"
145
+ }
146
+ },
147
+ "node_modules/@esbuild/freebsd-x64": {
148
+ "version": "0.21.5",
149
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
150
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
151
+ "cpu": [
152
+ "x64"
153
+ ],
154
+ "dev": true,
155
+ "license": "MIT",
156
+ "optional": true,
157
+ "os": [
158
+ "freebsd"
159
+ ],
160
+ "engines": {
161
+ "node": ">=12"
162
+ }
163
+ },
164
+ "node_modules/@esbuild/linux-arm": {
165
+ "version": "0.21.5",
166
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
167
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
168
+ "cpu": [
169
+ "arm"
170
+ ],
171
+ "dev": true,
172
+ "license": "MIT",
173
+ "optional": true,
174
+ "os": [
175
+ "linux"
176
+ ],
177
+ "engines": {
178
+ "node": ">=12"
179
+ }
180
+ },
181
+ "node_modules/@esbuild/linux-arm64": {
182
+ "version": "0.21.5",
183
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
184
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
185
+ "cpu": [
186
+ "arm64"
187
+ ],
188
+ "dev": true,
189
+ "license": "MIT",
190
+ "optional": true,
191
+ "os": [
192
+ "linux"
193
+ ],
194
+ "engines": {
195
+ "node": ">=12"
196
+ }
197
+ },
198
+ "node_modules/@esbuild/linux-ia32": {
199
+ "version": "0.21.5",
200
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
201
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
202
+ "cpu": [
203
+ "ia32"
204
+ ],
205
+ "dev": true,
206
+ "license": "MIT",
207
+ "optional": true,
208
+ "os": [
209
+ "linux"
210
+ ],
211
+ "engines": {
212
+ "node": ">=12"
213
+ }
214
+ },
215
+ "node_modules/@esbuild/linux-loong64": {
216
+ "version": "0.21.5",
217
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
218
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
219
+ "cpu": [
220
+ "loong64"
221
+ ],
222
+ "dev": true,
223
+ "license": "MIT",
224
+ "optional": true,
225
+ "os": [
226
+ "linux"
227
+ ],
228
+ "engines": {
229
+ "node": ">=12"
230
+ }
231
+ },
232
+ "node_modules/@esbuild/linux-mips64el": {
233
+ "version": "0.21.5",
234
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
235
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
236
+ "cpu": [
237
+ "mips64el"
238
+ ],
239
+ "dev": true,
240
+ "license": "MIT",
241
+ "optional": true,
242
+ "os": [
243
+ "linux"
244
+ ],
245
+ "engines": {
246
+ "node": ">=12"
247
+ }
248
+ },
249
+ "node_modules/@esbuild/linux-ppc64": {
250
+ "version": "0.21.5",
251
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
252
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
253
+ "cpu": [
254
+ "ppc64"
255
+ ],
256
+ "dev": true,
257
+ "license": "MIT",
258
+ "optional": true,
259
+ "os": [
260
+ "linux"
261
+ ],
262
+ "engines": {
263
+ "node": ">=12"
264
+ }
265
+ },
266
+ "node_modules/@esbuild/linux-riscv64": {
267
+ "version": "0.21.5",
268
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
269
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
270
+ "cpu": [
271
+ "riscv64"
272
+ ],
273
+ "dev": true,
274
+ "license": "MIT",
275
+ "optional": true,
276
+ "os": [
277
+ "linux"
278
+ ],
279
+ "engines": {
280
+ "node": ">=12"
281
+ }
282
+ },
283
+ "node_modules/@esbuild/linux-s390x": {
284
+ "version": "0.21.5",
285
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
286
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
287
+ "cpu": [
288
+ "s390x"
289
+ ],
290
+ "dev": true,
291
+ "license": "MIT",
292
+ "optional": true,
293
+ "os": [
294
+ "linux"
295
+ ],
296
+ "engines": {
297
+ "node": ">=12"
298
+ }
299
+ },
300
+ "node_modules/@esbuild/linux-x64": {
301
+ "version": "0.21.5",
302
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
303
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
304
+ "cpu": [
305
+ "x64"
306
+ ],
307
+ "dev": true,
308
+ "license": "MIT",
309
+ "optional": true,
310
+ "os": [
311
+ "linux"
312
+ ],
313
+ "engines": {
314
+ "node": ">=12"
315
+ }
316
+ },
317
+ "node_modules/@esbuild/netbsd-x64": {
318
+ "version": "0.21.5",
319
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
320
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
321
+ "cpu": [
322
+ "x64"
323
+ ],
324
+ "dev": true,
325
+ "license": "MIT",
326
+ "optional": true,
327
+ "os": [
328
+ "netbsd"
329
+ ],
330
+ "engines": {
331
+ "node": ">=12"
332
+ }
333
+ },
334
+ "node_modules/@esbuild/openbsd-x64": {
335
+ "version": "0.21.5",
336
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
337
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
338
+ "cpu": [
339
+ "x64"
340
+ ],
341
+ "dev": true,
342
+ "license": "MIT",
343
+ "optional": true,
344
+ "os": [
345
+ "openbsd"
346
+ ],
347
+ "engines": {
348
+ "node": ">=12"
349
+ }
350
+ },
351
+ "node_modules/@esbuild/sunos-x64": {
352
+ "version": "0.21.5",
353
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
354
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
355
+ "cpu": [
356
+ "x64"
357
+ ],
358
+ "dev": true,
359
+ "license": "MIT",
360
+ "optional": true,
361
+ "os": [
362
+ "sunos"
363
+ ],
364
+ "engines": {
365
+ "node": ">=12"
366
+ }
367
+ },
368
+ "node_modules/@esbuild/win32-arm64": {
369
+ "version": "0.21.5",
370
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
371
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
372
+ "cpu": [
373
+ "arm64"
374
+ ],
375
+ "dev": true,
376
+ "license": "MIT",
377
+ "optional": true,
378
+ "os": [
379
+ "win32"
380
+ ],
381
+ "engines": {
382
+ "node": ">=12"
383
+ }
384
+ },
385
+ "node_modules/@esbuild/win32-ia32": {
386
+ "version": "0.21.5",
387
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
388
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
389
+ "cpu": [
390
+ "ia32"
391
+ ],
392
+ "dev": true,
393
+ "license": "MIT",
394
+ "optional": true,
395
+ "os": [
396
+ "win32"
397
+ ],
398
+ "engines": {
399
+ "node": ">=12"
400
+ }
401
+ },
402
+ "node_modules/@esbuild/win32-x64": {
403
+ "version": "0.21.5",
404
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
405
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
406
+ "cpu": [
407
+ "x64"
408
+ ],
409
+ "dev": true,
410
+ "license": "MIT",
411
+ "optional": true,
412
+ "os": [
413
+ "win32"
414
+ ],
415
+ "engines": {
416
+ "node": ">=12"
417
+ }
418
+ },
419
+ "node_modules/@huggingface/jinja": {
420
+ "version": "0.5.3",
421
+ "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.3.tgz",
422
+ "integrity": "sha512-asqfZ4GQS0hD876Uw4qiUb7Tr/V5Q+JZuo2L+BtdrD4U40QU58nIRq3ZSgAzJgT874VLjhGVacaYfrdpXtEvtA==",
423
+ "license": "MIT",
424
+ "engines": {
425
+ "node": ">=18"
426
+ }
427
+ },
428
+ "node_modules/@huggingface/transformers": {
429
+ "version": "3.8.1",
430
+ "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.8.1.tgz",
431
+ "integrity": "sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==",
432
+ "license": "Apache-2.0",
433
+ "dependencies": {
434
+ "@huggingface/jinja": "^0.5.3",
435
+ "onnxruntime-node": "1.21.0",
436
+ "onnxruntime-web": "1.22.0-dev.20250409-89f8206ba4",
437
+ "sharp": "^0.34.1"
438
+ }
439
+ },
440
+ "node_modules/@huggingface/transformers/node_modules/onnxruntime-common": {
441
+ "version": "1.22.0-dev.20250409-89f8206ba4",
442
+ "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.22.0-dev.20250409-89f8206ba4.tgz",
443
+ "integrity": "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==",
444
+ "license": "MIT"
445
+ },
446
+ "node_modules/@huggingface/transformers/node_modules/onnxruntime-web": {
447
+ "version": "1.22.0-dev.20250409-89f8206ba4",
448
+ "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.22.0-dev.20250409-89f8206ba4.tgz",
449
+ "integrity": "sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==",
450
+ "license": "MIT",
451
+ "dependencies": {
452
+ "flatbuffers": "^25.1.24",
453
+ "guid-typescript": "^1.0.9",
454
+ "long": "^5.2.3",
455
+ "onnxruntime-common": "1.22.0-dev.20250409-89f8206ba4",
456
+ "platform": "^1.3.6",
457
+ "protobufjs": "^7.2.4"
458
+ }
459
+ },
460
+ "node_modules/@img/colour": {
461
+ "version": "1.0.0",
462
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
463
+ "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
464
+ "license": "MIT",
465
+ "engines": {
466
+ "node": ">=18"
467
+ }
468
+ },
469
+ "node_modules/@img/sharp-darwin-arm64": {
470
+ "version": "0.34.5",
471
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
472
+ "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
473
+ "cpu": [
474
+ "arm64"
475
+ ],
476
+ "license": "Apache-2.0",
477
+ "optional": true,
478
+ "os": [
479
+ "darwin"
480
+ ],
481
+ "engines": {
482
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
483
+ },
484
+ "funding": {
485
+ "url": "https://opencollective.com/libvips"
486
+ },
487
+ "optionalDependencies": {
488
+ "@img/sharp-libvips-darwin-arm64": "1.2.4"
489
+ }
490
+ },
491
+ "node_modules/@img/sharp-darwin-x64": {
492
+ "version": "0.34.5",
493
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
494
+ "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
495
+ "cpu": [
496
+ "x64"
497
+ ],
498
+ "license": "Apache-2.0",
499
+ "optional": true,
500
+ "os": [
501
+ "darwin"
502
+ ],
503
+ "engines": {
504
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
505
+ },
506
+ "funding": {
507
+ "url": "https://opencollective.com/libvips"
508
+ },
509
+ "optionalDependencies": {
510
+ "@img/sharp-libvips-darwin-x64": "1.2.4"
511
+ }
512
+ },
513
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
514
+ "version": "1.2.4",
515
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
516
+ "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
517
+ "cpu": [
518
+ "arm64"
519
+ ],
520
+ "license": "LGPL-3.0-or-later",
521
+ "optional": true,
522
+ "os": [
523
+ "darwin"
524
+ ],
525
+ "funding": {
526
+ "url": "https://opencollective.com/libvips"
527
+ }
528
+ },
529
+ "node_modules/@img/sharp-libvips-darwin-x64": {
530
+ "version": "1.2.4",
531
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
532
+ "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
533
+ "cpu": [
534
+ "x64"
535
+ ],
536
+ "license": "LGPL-3.0-or-later",
537
+ "optional": true,
538
+ "os": [
539
+ "darwin"
540
+ ],
541
+ "funding": {
542
+ "url": "https://opencollective.com/libvips"
543
+ }
544
+ },
545
+ "node_modules/@img/sharp-libvips-linux-arm": {
546
+ "version": "1.2.4",
547
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
548
+ "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
549
+ "cpu": [
550
+ "arm"
551
+ ],
552
+ "license": "LGPL-3.0-or-later",
553
+ "optional": true,
554
+ "os": [
555
+ "linux"
556
+ ],
557
+ "funding": {
558
+ "url": "https://opencollective.com/libvips"
559
+ }
560
+ },
561
+ "node_modules/@img/sharp-libvips-linux-arm64": {
562
+ "version": "1.2.4",
563
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
564
+ "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
565
+ "cpu": [
566
+ "arm64"
567
+ ],
568
+ "license": "LGPL-3.0-or-later",
569
+ "optional": true,
570
+ "os": [
571
+ "linux"
572
+ ],
573
+ "funding": {
574
+ "url": "https://opencollective.com/libvips"
575
+ }
576
+ },
577
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
578
+ "version": "1.2.4",
579
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
580
+ "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
581
+ "cpu": [
582
+ "ppc64"
583
+ ],
584
+ "license": "LGPL-3.0-or-later",
585
+ "optional": true,
586
+ "os": [
587
+ "linux"
588
+ ],
589
+ "funding": {
590
+ "url": "https://opencollective.com/libvips"
591
+ }
592
+ },
593
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
594
+ "version": "1.2.4",
595
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
596
+ "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
597
+ "cpu": [
598
+ "riscv64"
599
+ ],
600
+ "license": "LGPL-3.0-or-later",
601
+ "optional": true,
602
+ "os": [
603
+ "linux"
604
+ ],
605
+ "funding": {
606
+ "url": "https://opencollective.com/libvips"
607
+ }
608
+ },
609
+ "node_modules/@img/sharp-libvips-linux-s390x": {
610
+ "version": "1.2.4",
611
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
612
+ "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
613
+ "cpu": [
614
+ "s390x"
615
+ ],
616
+ "license": "LGPL-3.0-or-later",
617
+ "optional": true,
618
+ "os": [
619
+ "linux"
620
+ ],
621
+ "funding": {
622
+ "url": "https://opencollective.com/libvips"
623
+ }
624
+ },
625
+ "node_modules/@img/sharp-libvips-linux-x64": {
626
+ "version": "1.2.4",
627
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
628
+ "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
629
+ "cpu": [
630
+ "x64"
631
+ ],
632
+ "license": "LGPL-3.0-or-later",
633
+ "optional": true,
634
+ "os": [
635
+ "linux"
636
+ ],
637
+ "funding": {
638
+ "url": "https://opencollective.com/libvips"
639
+ }
640
+ },
641
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
642
+ "version": "1.2.4",
643
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
644
+ "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
645
+ "cpu": [
646
+ "arm64"
647
+ ],
648
+ "license": "LGPL-3.0-or-later",
649
+ "optional": true,
650
+ "os": [
651
+ "linux"
652
+ ],
653
+ "funding": {
654
+ "url": "https://opencollective.com/libvips"
655
+ }
656
+ },
657
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
658
+ "version": "1.2.4",
659
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
660
+ "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
661
+ "cpu": [
662
+ "x64"
663
+ ],
664
+ "license": "LGPL-3.0-or-later",
665
+ "optional": true,
666
+ "os": [
667
+ "linux"
668
+ ],
669
+ "funding": {
670
+ "url": "https://opencollective.com/libvips"
671
+ }
672
+ },
673
+ "node_modules/@img/sharp-linux-arm": {
674
+ "version": "0.34.5",
675
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
676
+ "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
677
+ "cpu": [
678
+ "arm"
679
+ ],
680
+ "license": "Apache-2.0",
681
+ "optional": true,
682
+ "os": [
683
+ "linux"
684
+ ],
685
+ "engines": {
686
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
687
+ },
688
+ "funding": {
689
+ "url": "https://opencollective.com/libvips"
690
+ },
691
+ "optionalDependencies": {
692
+ "@img/sharp-libvips-linux-arm": "1.2.4"
693
+ }
694
+ },
695
+ "node_modules/@img/sharp-linux-arm64": {
696
+ "version": "0.34.5",
697
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
698
+ "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
699
+ "cpu": [
700
+ "arm64"
701
+ ],
702
+ "license": "Apache-2.0",
703
+ "optional": true,
704
+ "os": [
705
+ "linux"
706
+ ],
707
+ "engines": {
708
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
709
+ },
710
+ "funding": {
711
+ "url": "https://opencollective.com/libvips"
712
+ },
713
+ "optionalDependencies": {
714
+ "@img/sharp-libvips-linux-arm64": "1.2.4"
715
+ }
716
+ },
717
+ "node_modules/@img/sharp-linux-ppc64": {
718
+ "version": "0.34.5",
719
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
720
+ "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
721
+ "cpu": [
722
+ "ppc64"
723
+ ],
724
+ "license": "Apache-2.0",
725
+ "optional": true,
726
+ "os": [
727
+ "linux"
728
+ ],
729
+ "engines": {
730
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
731
+ },
732
+ "funding": {
733
+ "url": "https://opencollective.com/libvips"
734
+ },
735
+ "optionalDependencies": {
736
+ "@img/sharp-libvips-linux-ppc64": "1.2.4"
737
+ }
738
+ },
739
+ "node_modules/@img/sharp-linux-riscv64": {
740
+ "version": "0.34.5",
741
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
742
+ "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
743
+ "cpu": [
744
+ "riscv64"
745
+ ],
746
+ "license": "Apache-2.0",
747
+ "optional": true,
748
+ "os": [
749
+ "linux"
750
+ ],
751
+ "engines": {
752
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
753
+ },
754
+ "funding": {
755
+ "url": "https://opencollective.com/libvips"
756
+ },
757
+ "optionalDependencies": {
758
+ "@img/sharp-libvips-linux-riscv64": "1.2.4"
759
+ }
760
+ },
761
+ "node_modules/@img/sharp-linux-s390x": {
762
+ "version": "0.34.5",
763
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
764
+ "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
765
+ "cpu": [
766
+ "s390x"
767
+ ],
768
+ "license": "Apache-2.0",
769
+ "optional": true,
770
+ "os": [
771
+ "linux"
772
+ ],
773
+ "engines": {
774
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
775
+ },
776
+ "funding": {
777
+ "url": "https://opencollective.com/libvips"
778
+ },
779
+ "optionalDependencies": {
780
+ "@img/sharp-libvips-linux-s390x": "1.2.4"
781
+ }
782
+ },
783
+ "node_modules/@img/sharp-linux-x64": {
784
+ "version": "0.34.5",
785
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
786
+ "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
787
+ "cpu": [
788
+ "x64"
789
+ ],
790
+ "license": "Apache-2.0",
791
+ "optional": true,
792
+ "os": [
793
+ "linux"
794
+ ],
795
+ "engines": {
796
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
797
+ },
798
+ "funding": {
799
+ "url": "https://opencollective.com/libvips"
800
+ },
801
+ "optionalDependencies": {
802
+ "@img/sharp-libvips-linux-x64": "1.2.4"
803
+ }
804
+ },
805
+ "node_modules/@img/sharp-linuxmusl-arm64": {
806
+ "version": "0.34.5",
807
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
808
+ "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
809
+ "cpu": [
810
+ "arm64"
811
+ ],
812
+ "license": "Apache-2.0",
813
+ "optional": true,
814
+ "os": [
815
+ "linux"
816
+ ],
817
+ "engines": {
818
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
819
+ },
820
+ "funding": {
821
+ "url": "https://opencollective.com/libvips"
822
+ },
823
+ "optionalDependencies": {
824
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
825
+ }
826
+ },
827
+ "node_modules/@img/sharp-linuxmusl-x64": {
828
+ "version": "0.34.5",
829
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
830
+ "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
831
+ "cpu": [
832
+ "x64"
833
+ ],
834
+ "license": "Apache-2.0",
835
+ "optional": true,
836
+ "os": [
837
+ "linux"
838
+ ],
839
+ "engines": {
840
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
841
+ },
842
+ "funding": {
843
+ "url": "https://opencollective.com/libvips"
844
+ },
845
+ "optionalDependencies": {
846
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
847
+ }
848
+ },
849
+ "node_modules/@img/sharp-wasm32": {
850
+ "version": "0.34.5",
851
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
852
+ "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
853
+ "cpu": [
854
+ "wasm32"
855
+ ],
856
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
857
+ "optional": true,
858
+ "dependencies": {
859
+ "@emnapi/runtime": "^1.7.0"
860
+ },
861
+ "engines": {
862
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
863
+ },
864
+ "funding": {
865
+ "url": "https://opencollective.com/libvips"
866
+ }
867
+ },
868
+ "node_modules/@img/sharp-win32-arm64": {
869
+ "version": "0.34.5",
870
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
871
+ "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
872
+ "cpu": [
873
+ "arm64"
874
+ ],
875
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
876
+ "optional": true,
877
+ "os": [
878
+ "win32"
879
+ ],
880
+ "engines": {
881
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
882
+ },
883
+ "funding": {
884
+ "url": "https://opencollective.com/libvips"
885
+ }
886
+ },
887
+ "node_modules/@img/sharp-win32-ia32": {
888
+ "version": "0.34.5",
889
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
890
+ "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
891
+ "cpu": [
892
+ "ia32"
893
+ ],
894
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
895
+ "optional": true,
896
+ "os": [
897
+ "win32"
898
+ ],
899
+ "engines": {
900
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
901
+ },
902
+ "funding": {
903
+ "url": "https://opencollective.com/libvips"
904
+ }
905
+ },
906
+ "node_modules/@img/sharp-win32-x64": {
907
+ "version": "0.34.5",
908
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
909
+ "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
910
+ "cpu": [
911
+ "x64"
912
+ ],
913
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
914
+ "optional": true,
915
+ "os": [
916
+ "win32"
917
+ ],
918
+ "engines": {
919
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
920
+ },
921
+ "funding": {
922
+ "url": "https://opencollective.com/libvips"
923
+ }
924
+ },
925
+ "node_modules/@isaacs/fs-minipass": {
926
+ "version": "4.0.1",
927
+ "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
928
+ "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
929
+ "license": "ISC",
930
+ "dependencies": {
931
+ "minipass": "^7.0.4"
932
+ },
933
+ "engines": {
934
+ "node": ">=18.0.0"
935
+ }
936
+ },
937
+ "node_modules/@protobufjs/aspromise": {
938
+ "version": "1.1.2",
939
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
940
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
941
+ "license": "BSD-3-Clause"
942
+ },
943
+ "node_modules/@protobufjs/base64": {
944
+ "version": "1.1.2",
945
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
946
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
947
+ "license": "BSD-3-Clause"
948
+ },
949
+ "node_modules/@protobufjs/codegen": {
950
+ "version": "2.0.4",
951
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
952
+ "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
953
+ "license": "BSD-3-Clause"
954
+ },
955
+ "node_modules/@protobufjs/eventemitter": {
956
+ "version": "1.1.0",
957
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
958
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
959
+ "license": "BSD-3-Clause"
960
+ },
961
+ "node_modules/@protobufjs/fetch": {
962
+ "version": "1.1.0",
963
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
964
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
965
+ "license": "BSD-3-Clause",
966
+ "dependencies": {
967
+ "@protobufjs/aspromise": "^1.1.1",
968
+ "@protobufjs/inquire": "^1.1.0"
969
+ }
970
+ },
971
+ "node_modules/@protobufjs/float": {
972
+ "version": "1.0.2",
973
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
974
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
975
+ "license": "BSD-3-Clause"
976
+ },
977
+ "node_modules/@protobufjs/inquire": {
978
+ "version": "1.1.0",
979
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
980
+ "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
981
+ "license": "BSD-3-Clause"
982
+ },
983
+ "node_modules/@protobufjs/path": {
984
+ "version": "1.1.2",
985
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
986
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
987
+ "license": "BSD-3-Clause"
988
+ },
989
+ "node_modules/@protobufjs/pool": {
990
+ "version": "1.1.0",
991
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
992
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
993
+ "license": "BSD-3-Clause"
994
+ },
995
+ "node_modules/@protobufjs/utf8": {
996
+ "version": "1.1.0",
997
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
998
+ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
999
+ "license": "BSD-3-Clause"
1000
+ },
1001
+ "node_modules/@rollup/rollup-android-arm-eabi": {
1002
+ "version": "4.54.0",
1003
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz",
1004
+ "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==",
1005
+ "cpu": [
1006
+ "arm"
1007
+ ],
1008
+ "dev": true,
1009
+ "license": "MIT",
1010
+ "optional": true,
1011
+ "os": [
1012
+ "android"
1013
+ ]
1014
+ },
1015
+ "node_modules/@rollup/rollup-android-arm64": {
1016
+ "version": "4.54.0",
1017
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz",
1018
+ "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==",
1019
+ "cpu": [
1020
+ "arm64"
1021
+ ],
1022
+ "dev": true,
1023
+ "license": "MIT",
1024
+ "optional": true,
1025
+ "os": [
1026
+ "android"
1027
+ ]
1028
+ },
1029
+ "node_modules/@rollup/rollup-darwin-arm64": {
1030
+ "version": "4.54.0",
1031
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz",
1032
+ "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==",
1033
+ "cpu": [
1034
+ "arm64"
1035
+ ],
1036
+ "dev": true,
1037
+ "license": "MIT",
1038
+ "optional": true,
1039
+ "os": [
1040
+ "darwin"
1041
+ ]
1042
+ },
1043
+ "node_modules/@rollup/rollup-darwin-x64": {
1044
+ "version": "4.54.0",
1045
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz",
1046
+ "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==",
1047
+ "cpu": [
1048
+ "x64"
1049
+ ],
1050
+ "dev": true,
1051
+ "license": "MIT",
1052
+ "optional": true,
1053
+ "os": [
1054
+ "darwin"
1055
+ ]
1056
+ },
1057
+ "node_modules/@rollup/rollup-freebsd-arm64": {
1058
+ "version": "4.54.0",
1059
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz",
1060
+ "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==",
1061
+ "cpu": [
1062
+ "arm64"
1063
+ ],
1064
+ "dev": true,
1065
+ "license": "MIT",
1066
+ "optional": true,
1067
+ "os": [
1068
+ "freebsd"
1069
+ ]
1070
+ },
1071
+ "node_modules/@rollup/rollup-freebsd-x64": {
1072
+ "version": "4.54.0",
1073
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz",
1074
+ "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==",
1075
+ "cpu": [
1076
+ "x64"
1077
+ ],
1078
+ "dev": true,
1079
+ "license": "MIT",
1080
+ "optional": true,
1081
+ "os": [
1082
+ "freebsd"
1083
+ ]
1084
+ },
1085
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
1086
+ "version": "4.54.0",
1087
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz",
1088
+ "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==",
1089
+ "cpu": [
1090
+ "arm"
1091
+ ],
1092
+ "dev": true,
1093
+ "license": "MIT",
1094
+ "optional": true,
1095
+ "os": [
1096
+ "linux"
1097
+ ]
1098
+ },
1099
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
1100
+ "version": "4.54.0",
1101
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz",
1102
+ "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==",
1103
+ "cpu": [
1104
+ "arm"
1105
+ ],
1106
+ "dev": true,
1107
+ "license": "MIT",
1108
+ "optional": true,
1109
+ "os": [
1110
+ "linux"
1111
+ ]
1112
+ },
1113
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
1114
+ "version": "4.54.0",
1115
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz",
1116
+ "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==",
1117
+ "cpu": [
1118
+ "arm64"
1119
+ ],
1120
+ "dev": true,
1121
+ "license": "MIT",
1122
+ "optional": true,
1123
+ "os": [
1124
+ "linux"
1125
+ ]
1126
+ },
1127
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
1128
+ "version": "4.54.0",
1129
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz",
1130
+ "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==",
1131
+ "cpu": [
1132
+ "arm64"
1133
+ ],
1134
+ "dev": true,
1135
+ "license": "MIT",
1136
+ "optional": true,
1137
+ "os": [
1138
+ "linux"
1139
+ ]
1140
+ },
1141
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
1142
+ "version": "4.54.0",
1143
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz",
1144
+ "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==",
1145
+ "cpu": [
1146
+ "loong64"
1147
+ ],
1148
+ "dev": true,
1149
+ "license": "MIT",
1150
+ "optional": true,
1151
+ "os": [
1152
+ "linux"
1153
+ ]
1154
+ },
1155
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
1156
+ "version": "4.54.0",
1157
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz",
1158
+ "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==",
1159
+ "cpu": [
1160
+ "ppc64"
1161
+ ],
1162
+ "dev": true,
1163
+ "license": "MIT",
1164
+ "optional": true,
1165
+ "os": [
1166
+ "linux"
1167
+ ]
1168
+ },
1169
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
1170
+ "version": "4.54.0",
1171
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz",
1172
+ "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==",
1173
+ "cpu": [
1174
+ "riscv64"
1175
+ ],
1176
+ "dev": true,
1177
+ "license": "MIT",
1178
+ "optional": true,
1179
+ "os": [
1180
+ "linux"
1181
+ ]
1182
+ },
1183
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
1184
+ "version": "4.54.0",
1185
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz",
1186
+ "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==",
1187
+ "cpu": [
1188
+ "riscv64"
1189
+ ],
1190
+ "dev": true,
1191
+ "license": "MIT",
1192
+ "optional": true,
1193
+ "os": [
1194
+ "linux"
1195
+ ]
1196
+ },
1197
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
1198
+ "version": "4.54.0",
1199
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz",
1200
+ "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==",
1201
+ "cpu": [
1202
+ "s390x"
1203
+ ],
1204
+ "dev": true,
1205
+ "license": "MIT",
1206
+ "optional": true,
1207
+ "os": [
1208
+ "linux"
1209
+ ]
1210
+ },
1211
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
1212
+ "version": "4.54.0",
1213
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz",
1214
+ "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==",
1215
+ "cpu": [
1216
+ "x64"
1217
+ ],
1218
+ "dev": true,
1219
+ "license": "MIT",
1220
+ "optional": true,
1221
+ "os": [
1222
+ "linux"
1223
+ ]
1224
+ },
1225
+ "node_modules/@rollup/rollup-linux-x64-musl": {
1226
+ "version": "4.54.0",
1227
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz",
1228
+ "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==",
1229
+ "cpu": [
1230
+ "x64"
1231
+ ],
1232
+ "dev": true,
1233
+ "license": "MIT",
1234
+ "optional": true,
1235
+ "os": [
1236
+ "linux"
1237
+ ]
1238
+ },
1239
+ "node_modules/@rollup/rollup-openharmony-arm64": {
1240
+ "version": "4.54.0",
1241
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz",
1242
+ "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==",
1243
+ "cpu": [
1244
+ "arm64"
1245
+ ],
1246
+ "dev": true,
1247
+ "license": "MIT",
1248
+ "optional": true,
1249
+ "os": [
1250
+ "openharmony"
1251
+ ]
1252
+ },
1253
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1254
+ "version": "4.54.0",
1255
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz",
1256
+ "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==",
1257
+ "cpu": [
1258
+ "arm64"
1259
+ ],
1260
+ "dev": true,
1261
+ "license": "MIT",
1262
+ "optional": true,
1263
+ "os": [
1264
+ "win32"
1265
+ ]
1266
+ },
1267
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1268
+ "version": "4.54.0",
1269
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz",
1270
+ "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==",
1271
+ "cpu": [
1272
+ "ia32"
1273
+ ],
1274
+ "dev": true,
1275
+ "license": "MIT",
1276
+ "optional": true,
1277
+ "os": [
1278
+ "win32"
1279
+ ]
1280
+ },
1281
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1282
+ "version": "4.54.0",
1283
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz",
1284
+ "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==",
1285
+ "cpu": [
1286
+ "x64"
1287
+ ],
1288
+ "dev": true,
1289
+ "license": "MIT",
1290
+ "optional": true,
1291
+ "os": [
1292
+ "win32"
1293
+ ]
1294
+ },
1295
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1296
+ "version": "4.54.0",
1297
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz",
1298
+ "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==",
1299
+ "cpu": [
1300
+ "x64"
1301
+ ],
1302
+ "dev": true,
1303
+ "license": "MIT",
1304
+ "optional": true,
1305
+ "os": [
1306
+ "win32"
1307
+ ]
1308
+ },
1309
+ "node_modules/@types/estree": {
1310
+ "version": "1.0.8",
1311
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
1312
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
1313
+ "dev": true,
1314
+ "license": "MIT"
1315
+ },
1316
+ "node_modules/@types/node": {
1317
+ "version": "25.0.3",
1318
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
1319
+ "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
1320
+ "license": "MIT",
1321
+ "dependencies": {
1322
+ "undici-types": "~7.16.0"
1323
+ }
1324
+ },
1325
+ "node_modules/boolean": {
1326
+ "version": "3.2.0",
1327
+ "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
1328
+ "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
1329
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
1330
+ "license": "MIT"
1331
+ },
1332
+ "node_modules/chownr": {
1333
+ "version": "3.0.0",
1334
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
1335
+ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
1336
+ "license": "BlueOak-1.0.0",
1337
+ "engines": {
1338
+ "node": ">=18"
1339
+ }
1340
+ },
1341
+ "node_modules/define-data-property": {
1342
+ "version": "1.1.4",
1343
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
1344
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
1345
+ "license": "MIT",
1346
+ "dependencies": {
1347
+ "es-define-property": "^1.0.0",
1348
+ "es-errors": "^1.3.0",
1349
+ "gopd": "^1.0.1"
1350
+ },
1351
+ "engines": {
1352
+ "node": ">= 0.4"
1353
+ },
1354
+ "funding": {
1355
+ "url": "https://github.com/sponsors/ljharb"
1356
+ }
1357
+ },
1358
+ "node_modules/define-properties": {
1359
+ "version": "1.2.1",
1360
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
1361
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
1362
+ "license": "MIT",
1363
+ "dependencies": {
1364
+ "define-data-property": "^1.0.1",
1365
+ "has-property-descriptors": "^1.0.0",
1366
+ "object-keys": "^1.1.1"
1367
+ },
1368
+ "engines": {
1369
+ "node": ">= 0.4"
1370
+ },
1371
+ "funding": {
1372
+ "url": "https://github.com/sponsors/ljharb"
1373
+ }
1374
+ },
1375
+ "node_modules/detect-libc": {
1376
+ "version": "2.1.2",
1377
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
1378
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
1379
+ "license": "Apache-2.0",
1380
+ "engines": {
1381
+ "node": ">=8"
1382
+ }
1383
+ },
1384
+ "node_modules/detect-node": {
1385
+ "version": "2.1.0",
1386
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
1387
+ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
1388
+ "license": "MIT"
1389
+ },
1390
+ "node_modules/es-define-property": {
1391
+ "version": "1.0.1",
1392
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
1393
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
1394
+ "license": "MIT",
1395
+ "engines": {
1396
+ "node": ">= 0.4"
1397
+ }
1398
+ },
1399
+ "node_modules/es-errors": {
1400
+ "version": "1.3.0",
1401
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
1402
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
1403
+ "license": "MIT",
1404
+ "engines": {
1405
+ "node": ">= 0.4"
1406
+ }
1407
+ },
1408
+ "node_modules/es6-error": {
1409
+ "version": "4.1.1",
1410
+ "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
1411
+ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
1412
+ "license": "MIT"
1413
+ },
1414
+ "node_modules/esbuild": {
1415
+ "version": "0.21.5",
1416
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
1417
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
1418
+ "dev": true,
1419
+ "hasInstallScript": true,
1420
+ "license": "MIT",
1421
+ "bin": {
1422
+ "esbuild": "bin/esbuild"
1423
+ },
1424
+ "engines": {
1425
+ "node": ">=12"
1426
+ },
1427
+ "optionalDependencies": {
1428
+ "@esbuild/aix-ppc64": "0.21.5",
1429
+ "@esbuild/android-arm": "0.21.5",
1430
+ "@esbuild/android-arm64": "0.21.5",
1431
+ "@esbuild/android-x64": "0.21.5",
1432
+ "@esbuild/darwin-arm64": "0.21.5",
1433
+ "@esbuild/darwin-x64": "0.21.5",
1434
+ "@esbuild/freebsd-arm64": "0.21.5",
1435
+ "@esbuild/freebsd-x64": "0.21.5",
1436
+ "@esbuild/linux-arm": "0.21.5",
1437
+ "@esbuild/linux-arm64": "0.21.5",
1438
+ "@esbuild/linux-ia32": "0.21.5",
1439
+ "@esbuild/linux-loong64": "0.21.5",
1440
+ "@esbuild/linux-mips64el": "0.21.5",
1441
+ "@esbuild/linux-ppc64": "0.21.5",
1442
+ "@esbuild/linux-riscv64": "0.21.5",
1443
+ "@esbuild/linux-s390x": "0.21.5",
1444
+ "@esbuild/linux-x64": "0.21.5",
1445
+ "@esbuild/netbsd-x64": "0.21.5",
1446
+ "@esbuild/openbsd-x64": "0.21.5",
1447
+ "@esbuild/sunos-x64": "0.21.5",
1448
+ "@esbuild/win32-arm64": "0.21.5",
1449
+ "@esbuild/win32-ia32": "0.21.5",
1450
+ "@esbuild/win32-x64": "0.21.5"
1451
+ }
1452
+ },
1453
+ "node_modules/escape-string-regexp": {
1454
+ "version": "4.0.0",
1455
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
1456
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
1457
+ "license": "MIT",
1458
+ "engines": {
1459
+ "node": ">=10"
1460
+ },
1461
+ "funding": {
1462
+ "url": "https://github.com/sponsors/sindresorhus"
1463
+ }
1464
+ },
1465
+ "node_modules/flatbuffers": {
1466
+ "version": "25.9.23",
1467
+ "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz",
1468
+ "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==",
1469
+ "license": "Apache-2.0"
1470
+ },
1471
+ "node_modules/fsevents": {
1472
+ "version": "2.3.3",
1473
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1474
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1475
+ "dev": true,
1476
+ "hasInstallScript": true,
1477
+ "license": "MIT",
1478
+ "optional": true,
1479
+ "os": [
1480
+ "darwin"
1481
+ ],
1482
+ "engines": {
1483
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1484
+ }
1485
+ },
1486
+ "node_modules/global-agent": {
1487
+ "version": "3.0.0",
1488
+ "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
1489
+ "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
1490
+ "license": "BSD-3-Clause",
1491
+ "dependencies": {
1492
+ "boolean": "^3.0.1",
1493
+ "es6-error": "^4.1.1",
1494
+ "matcher": "^3.0.0",
1495
+ "roarr": "^2.15.3",
1496
+ "semver": "^7.3.2",
1497
+ "serialize-error": "^7.0.1"
1498
+ },
1499
+ "engines": {
1500
+ "node": ">=10.0"
1501
+ }
1502
+ },
1503
+ "node_modules/globalthis": {
1504
+ "version": "1.0.4",
1505
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
1506
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
1507
+ "license": "MIT",
1508
+ "dependencies": {
1509
+ "define-properties": "^1.2.1",
1510
+ "gopd": "^1.0.1"
1511
+ },
1512
+ "engines": {
1513
+ "node": ">= 0.4"
1514
+ },
1515
+ "funding": {
1516
+ "url": "https://github.com/sponsors/ljharb"
1517
+ }
1518
+ },
1519
+ "node_modules/gopd": {
1520
+ "version": "1.2.0",
1521
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
1522
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
1523
+ "license": "MIT",
1524
+ "engines": {
1525
+ "node": ">= 0.4"
1526
+ },
1527
+ "funding": {
1528
+ "url": "https://github.com/sponsors/ljharb"
1529
+ }
1530
+ },
1531
+ "node_modules/guid-typescript": {
1532
+ "version": "1.0.9",
1533
+ "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz",
1534
+ "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==",
1535
+ "license": "ISC"
1536
+ },
1537
+ "node_modules/has-property-descriptors": {
1538
+ "version": "1.0.2",
1539
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
1540
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
1541
+ "license": "MIT",
1542
+ "dependencies": {
1543
+ "es-define-property": "^1.0.0"
1544
+ },
1545
+ "funding": {
1546
+ "url": "https://github.com/sponsors/ljharb"
1547
+ }
1548
+ },
1549
+ "node_modules/json-stringify-safe": {
1550
+ "version": "5.0.1",
1551
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
1552
+ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
1553
+ "license": "ISC"
1554
+ },
1555
+ "node_modules/long": {
1556
+ "version": "5.3.2",
1557
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
1558
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
1559
+ "license": "Apache-2.0"
1560
+ },
1561
+ "node_modules/matcher": {
1562
+ "version": "3.0.0",
1563
+ "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
1564
+ "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
1565
+ "license": "MIT",
1566
+ "dependencies": {
1567
+ "escape-string-regexp": "^4.0.0"
1568
+ },
1569
+ "engines": {
1570
+ "node": ">=10"
1571
+ }
1572
+ },
1573
+ "node_modules/minipass": {
1574
+ "version": "7.1.2",
1575
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
1576
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
1577
+ "license": "ISC",
1578
+ "engines": {
1579
+ "node": ">=16 || 14 >=14.17"
1580
+ }
1581
+ },
1582
+ "node_modules/minizlib": {
1583
+ "version": "3.1.0",
1584
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
1585
+ "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
1586
+ "license": "MIT",
1587
+ "dependencies": {
1588
+ "minipass": "^7.1.2"
1589
+ },
1590
+ "engines": {
1591
+ "node": ">= 18"
1592
+ }
1593
+ },
1594
+ "node_modules/nanoid": {
1595
+ "version": "3.3.11",
1596
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
1597
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1598
+ "dev": true,
1599
+ "funding": [
1600
+ {
1601
+ "type": "github",
1602
+ "url": "https://github.com/sponsors/ai"
1603
+ }
1604
+ ],
1605
+ "license": "MIT",
1606
+ "bin": {
1607
+ "nanoid": "bin/nanoid.cjs"
1608
+ },
1609
+ "engines": {
1610
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1611
+ }
1612
+ },
1613
+ "node_modules/object-keys": {
1614
+ "version": "1.1.1",
1615
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
1616
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
1617
+ "license": "MIT",
1618
+ "engines": {
1619
+ "node": ">= 0.4"
1620
+ }
1621
+ },
1622
+ "node_modules/onnxruntime-common": {
1623
+ "version": "1.21.0",
1624
+ "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz",
1625
+ "integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==",
1626
+ "license": "MIT"
1627
+ },
1628
+ "node_modules/onnxruntime-node": {
1629
+ "version": "1.21.0",
1630
+ "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.21.0.tgz",
1631
+ "integrity": "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==",
1632
+ "hasInstallScript": true,
1633
+ "license": "MIT",
1634
+ "os": [
1635
+ "win32",
1636
+ "darwin",
1637
+ "linux"
1638
+ ],
1639
+ "dependencies": {
1640
+ "global-agent": "^3.0.0",
1641
+ "onnxruntime-common": "1.21.0",
1642
+ "tar": "^7.0.1"
1643
+ }
1644
+ },
1645
+ "node_modules/onnxruntime-web": {
1646
+ "version": "1.23.2",
1647
+ "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.23.2.tgz",
1648
+ "integrity": "sha512-T09JUtMn+CZLk3mFwqiH0lgQf+4S7+oYHHtk6uhaYAAJI95bTcKi5bOOZYwORXfS/RLZCjDDEXGWIuOCAFlEjg==",
1649
+ "license": "MIT",
1650
+ "dependencies": {
1651
+ "flatbuffers": "^25.1.24",
1652
+ "guid-typescript": "^1.0.9",
1653
+ "long": "^5.2.3",
1654
+ "onnxruntime-common": "1.23.2",
1655
+ "platform": "^1.3.6",
1656
+ "protobufjs": "^7.2.4"
1657
+ }
1658
+ },
1659
+ "node_modules/onnxruntime-web/node_modules/onnxruntime-common": {
1660
+ "version": "1.23.2",
1661
+ "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.23.2.tgz",
1662
+ "integrity": "sha512-5LFsC9Dukzp2WV6kNHYLNzp8sT6V02IubLCbzw2Xd6X5GOlr65gAX6xiJwyi2URJol/s71gaQLC5F2C25AAR2w==",
1663
+ "license": "MIT"
1664
+ },
1665
+ "node_modules/picocolors": {
1666
+ "version": "1.1.1",
1667
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1668
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1669
+ "dev": true,
1670
+ "license": "ISC"
1671
+ },
1672
+ "node_modules/platform": {
1673
+ "version": "1.3.6",
1674
+ "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
1675
+ "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
1676
+ "license": "MIT"
1677
+ },
1678
+ "node_modules/postcss": {
1679
+ "version": "8.5.6",
1680
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
1681
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
1682
+ "dev": true,
1683
+ "funding": [
1684
+ {
1685
+ "type": "opencollective",
1686
+ "url": "https://opencollective.com/postcss/"
1687
+ },
1688
+ {
1689
+ "type": "tidelift",
1690
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1691
+ },
1692
+ {
1693
+ "type": "github",
1694
+ "url": "https://github.com/sponsors/ai"
1695
+ }
1696
+ ],
1697
+ "license": "MIT",
1698
+ "dependencies": {
1699
+ "nanoid": "^3.3.11",
1700
+ "picocolors": "^1.1.1",
1701
+ "source-map-js": "^1.2.1"
1702
+ },
1703
+ "engines": {
1704
+ "node": "^10 || ^12 || >=14"
1705
+ }
1706
+ },
1707
+ "node_modules/protobufjs": {
1708
+ "version": "7.5.4",
1709
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
1710
+ "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
1711
+ "hasInstallScript": true,
1712
+ "license": "BSD-3-Clause",
1713
+ "dependencies": {
1714
+ "@protobufjs/aspromise": "^1.1.2",
1715
+ "@protobufjs/base64": "^1.1.2",
1716
+ "@protobufjs/codegen": "^2.0.4",
1717
+ "@protobufjs/eventemitter": "^1.1.0",
1718
+ "@protobufjs/fetch": "^1.1.0",
1719
+ "@protobufjs/float": "^1.0.2",
1720
+ "@protobufjs/inquire": "^1.1.0",
1721
+ "@protobufjs/path": "^1.1.2",
1722
+ "@protobufjs/pool": "^1.1.0",
1723
+ "@protobufjs/utf8": "^1.1.0",
1724
+ "@types/node": ">=13.7.0",
1725
+ "long": "^5.0.0"
1726
+ },
1727
+ "engines": {
1728
+ "node": ">=12.0.0"
1729
+ }
1730
+ },
1731
+ "node_modules/roarr": {
1732
+ "version": "2.15.4",
1733
+ "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
1734
+ "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
1735
+ "license": "BSD-3-Clause",
1736
+ "dependencies": {
1737
+ "boolean": "^3.0.1",
1738
+ "detect-node": "^2.0.4",
1739
+ "globalthis": "^1.0.1",
1740
+ "json-stringify-safe": "^5.0.1",
1741
+ "semver-compare": "^1.0.0",
1742
+ "sprintf-js": "^1.1.2"
1743
+ },
1744
+ "engines": {
1745
+ "node": ">=8.0"
1746
+ }
1747
+ },
1748
+ "node_modules/rollup": {
1749
+ "version": "4.54.0",
1750
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz",
1751
+ "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
1752
+ "dev": true,
1753
+ "license": "MIT",
1754
+ "dependencies": {
1755
+ "@types/estree": "1.0.8"
1756
+ },
1757
+ "bin": {
1758
+ "rollup": "dist/bin/rollup"
1759
+ },
1760
+ "engines": {
1761
+ "node": ">=18.0.0",
1762
+ "npm": ">=8.0.0"
1763
+ },
1764
+ "optionalDependencies": {
1765
+ "@rollup/rollup-android-arm-eabi": "4.54.0",
1766
+ "@rollup/rollup-android-arm64": "4.54.0",
1767
+ "@rollup/rollup-darwin-arm64": "4.54.0",
1768
+ "@rollup/rollup-darwin-x64": "4.54.0",
1769
+ "@rollup/rollup-freebsd-arm64": "4.54.0",
1770
+ "@rollup/rollup-freebsd-x64": "4.54.0",
1771
+ "@rollup/rollup-linux-arm-gnueabihf": "4.54.0",
1772
+ "@rollup/rollup-linux-arm-musleabihf": "4.54.0",
1773
+ "@rollup/rollup-linux-arm64-gnu": "4.54.0",
1774
+ "@rollup/rollup-linux-arm64-musl": "4.54.0",
1775
+ "@rollup/rollup-linux-loong64-gnu": "4.54.0",
1776
+ "@rollup/rollup-linux-ppc64-gnu": "4.54.0",
1777
+ "@rollup/rollup-linux-riscv64-gnu": "4.54.0",
1778
+ "@rollup/rollup-linux-riscv64-musl": "4.54.0",
1779
+ "@rollup/rollup-linux-s390x-gnu": "4.54.0",
1780
+ "@rollup/rollup-linux-x64-gnu": "4.54.0",
1781
+ "@rollup/rollup-linux-x64-musl": "4.54.0",
1782
+ "@rollup/rollup-openharmony-arm64": "4.54.0",
1783
+ "@rollup/rollup-win32-arm64-msvc": "4.54.0",
1784
+ "@rollup/rollup-win32-ia32-msvc": "4.54.0",
1785
+ "@rollup/rollup-win32-x64-gnu": "4.54.0",
1786
+ "@rollup/rollup-win32-x64-msvc": "4.54.0",
1787
+ "fsevents": "~2.3.2"
1788
+ }
1789
+ },
1790
+ "node_modules/semver": {
1791
+ "version": "7.7.3",
1792
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
1793
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
1794
+ "license": "ISC",
1795
+ "bin": {
1796
+ "semver": "bin/semver.js"
1797
+ },
1798
+ "engines": {
1799
+ "node": ">=10"
1800
+ }
1801
+ },
1802
+ "node_modules/semver-compare": {
1803
+ "version": "1.0.0",
1804
+ "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
1805
+ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
1806
+ "license": "MIT"
1807
+ },
1808
+ "node_modules/serialize-error": {
1809
+ "version": "7.0.1",
1810
+ "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
1811
+ "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
1812
+ "license": "MIT",
1813
+ "dependencies": {
1814
+ "type-fest": "^0.13.1"
1815
+ },
1816
+ "engines": {
1817
+ "node": ">=10"
1818
+ },
1819
+ "funding": {
1820
+ "url": "https://github.com/sponsors/sindresorhus"
1821
+ }
1822
+ },
1823
+ "node_modules/sharp": {
1824
+ "version": "0.34.5",
1825
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
1826
+ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
1827
+ "hasInstallScript": true,
1828
+ "license": "Apache-2.0",
1829
+ "dependencies": {
1830
+ "@img/colour": "^1.0.0",
1831
+ "detect-libc": "^2.1.2",
1832
+ "semver": "^7.7.3"
1833
+ },
1834
+ "engines": {
1835
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1836
+ },
1837
+ "funding": {
1838
+ "url": "https://opencollective.com/libvips"
1839
+ },
1840
+ "optionalDependencies": {
1841
+ "@img/sharp-darwin-arm64": "0.34.5",
1842
+ "@img/sharp-darwin-x64": "0.34.5",
1843
+ "@img/sharp-libvips-darwin-arm64": "1.2.4",
1844
+ "@img/sharp-libvips-darwin-x64": "1.2.4",
1845
+ "@img/sharp-libvips-linux-arm": "1.2.4",
1846
+ "@img/sharp-libvips-linux-arm64": "1.2.4",
1847
+ "@img/sharp-libvips-linux-ppc64": "1.2.4",
1848
+ "@img/sharp-libvips-linux-riscv64": "1.2.4",
1849
+ "@img/sharp-libvips-linux-s390x": "1.2.4",
1850
+ "@img/sharp-libvips-linux-x64": "1.2.4",
1851
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
1852
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
1853
+ "@img/sharp-linux-arm": "0.34.5",
1854
+ "@img/sharp-linux-arm64": "0.34.5",
1855
+ "@img/sharp-linux-ppc64": "0.34.5",
1856
+ "@img/sharp-linux-riscv64": "0.34.5",
1857
+ "@img/sharp-linux-s390x": "0.34.5",
1858
+ "@img/sharp-linux-x64": "0.34.5",
1859
+ "@img/sharp-linuxmusl-arm64": "0.34.5",
1860
+ "@img/sharp-linuxmusl-x64": "0.34.5",
1861
+ "@img/sharp-wasm32": "0.34.5",
1862
+ "@img/sharp-win32-arm64": "0.34.5",
1863
+ "@img/sharp-win32-ia32": "0.34.5",
1864
+ "@img/sharp-win32-x64": "0.34.5"
1865
+ }
1866
+ },
1867
+ "node_modules/source-map-js": {
1868
+ "version": "1.2.1",
1869
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1870
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1871
+ "dev": true,
1872
+ "license": "BSD-3-Clause",
1873
+ "engines": {
1874
+ "node": ">=0.10.0"
1875
+ }
1876
+ },
1877
+ "node_modules/sprintf-js": {
1878
+ "version": "1.1.3",
1879
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
1880
+ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
1881
+ "license": "BSD-3-Clause"
1882
+ },
1883
+ "node_modules/tar": {
1884
+ "version": "7.5.2",
1885
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz",
1886
+ "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==",
1887
+ "license": "BlueOak-1.0.0",
1888
+ "dependencies": {
1889
+ "@isaacs/fs-minipass": "^4.0.0",
1890
+ "chownr": "^3.0.0",
1891
+ "minipass": "^7.1.2",
1892
+ "minizlib": "^3.1.0",
1893
+ "yallist": "^5.0.0"
1894
+ },
1895
+ "engines": {
1896
+ "node": ">=18"
1897
+ }
1898
+ },
1899
+ "node_modules/tslib": {
1900
+ "version": "2.8.1",
1901
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
1902
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
1903
+ "license": "0BSD",
1904
+ "optional": true
1905
+ },
1906
+ "node_modules/type-fest": {
1907
+ "version": "0.13.1",
1908
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
1909
+ "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
1910
+ "license": "(MIT OR CC0-1.0)",
1911
+ "engines": {
1912
+ "node": ">=10"
1913
+ },
1914
+ "funding": {
1915
+ "url": "https://github.com/sponsors/sindresorhus"
1916
+ }
1917
+ },
1918
+ "node_modules/undici-types": {
1919
+ "version": "7.16.0",
1920
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
1921
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
1922
+ "license": "MIT"
1923
+ },
1924
+ "node_modules/vite": {
1925
+ "version": "5.4.21",
1926
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
1927
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
1928
+ "dev": true,
1929
+ "license": "MIT",
1930
+ "dependencies": {
1931
+ "esbuild": "^0.21.3",
1932
+ "postcss": "^8.4.43",
1933
+ "rollup": "^4.20.0"
1934
+ },
1935
+ "bin": {
1936
+ "vite": "bin/vite.js"
1937
+ },
1938
+ "engines": {
1939
+ "node": "^18.0.0 || >=20.0.0"
1940
+ },
1941
+ "funding": {
1942
+ "url": "https://github.com/vitejs/vite?sponsor=1"
1943
+ },
1944
+ "optionalDependencies": {
1945
+ "fsevents": "~2.3.3"
1946
+ },
1947
+ "peerDependencies": {
1948
+ "@types/node": "^18.0.0 || >=20.0.0",
1949
+ "less": "*",
1950
+ "lightningcss": "^1.21.0",
1951
+ "sass": "*",
1952
+ "sass-embedded": "*",
1953
+ "stylus": "*",
1954
+ "sugarss": "*",
1955
+ "terser": "^5.4.0"
1956
+ },
1957
+ "peerDependenciesMeta": {
1958
+ "@types/node": {
1959
+ "optional": true
1960
+ },
1961
+ "less": {
1962
+ "optional": true
1963
+ },
1964
+ "lightningcss": {
1965
+ "optional": true
1966
+ },
1967
+ "sass": {
1968
+ "optional": true
1969
+ },
1970
+ "sass-embedded": {
1971
+ "optional": true
1972
+ },
1973
+ "stylus": {
1974
+ "optional": true
1975
+ },
1976
+ "sugarss": {
1977
+ "optional": true
1978
+ },
1979
+ "terser": {
1980
+ "optional": true
1981
+ }
1982
+ }
1983
+ },
1984
+ "node_modules/yallist": {
1985
+ "version": "5.0.0",
1986
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
1987
+ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
1988
+ "license": "BlueOak-1.0.0",
1989
+ "engines": {
1990
+ "node": ">=18"
1991
+ }
1992
+ }
1993
+ }
1994
+ }
package.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "lfm25-vl-webgpu",
3
+ "version": "1.0.0",
4
+ "description": "LFM2.5-VL Vision-Language Demo with WebGPU",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@huggingface/transformers": "^3.7.1",
13
+ "onnxruntime-web": "^1.23.2"
14
+ },
15
+ "devDependencies": {
16
+ "vite": "^5.4.0"
17
+ }
18
+ }
styles.css ADDED
@@ -0,0 +1,1103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Brand color system for VLM Demo */
2
+
3
+ :root {
4
+ /* Base brand colors */
5
+ --white: rgb(255, 255, 255);
6
+ --black: rgb(0, 0, 0);
7
+ --light-purple: rgb(205, 130, 240);
8
+ --purple: rgb(85, 5, 75);
9
+ --orange: rgb(255, 95, 30);
10
+
11
+ /* Alpha variations */
12
+ --white-70: rgba(255, 255, 255, 0.7);
13
+ --white-50: rgba(255, 255, 255, 0.5);
14
+ --white-30: rgba(255, 255, 255, 0.3);
15
+ --white-10: rgba(255, 255, 255, 0.1);
16
+
17
+ --black-70: rgba(0, 0, 0, 0.7);
18
+ --black-50: rgba(0, 0, 0, 0.5);
19
+ --black-30: rgba(0, 0, 0, 0.3);
20
+ --black-10: rgba(0, 0, 0, 0.1);
21
+
22
+ --light-purple-70: rgba(205, 130, 240, 0.7);
23
+ --light-purple-50: rgba(205, 130, 240, 0.5);
24
+ --light-purple-30: rgba(205, 130, 240, 0.3);
25
+ --light-purple-10: rgba(205, 130, 240, 0.1);
26
+
27
+ --purple-70: rgba(85, 5, 75, 0.7);
28
+ --purple-50: rgba(85, 5, 75, 0.5);
29
+ --purple-30: rgba(85, 5, 75, 0.3);
30
+ --purple-10: rgba(85, 5, 75, 0.1);
31
+
32
+ --orange-70: rgba(255, 95, 30, 0.7);
33
+ --orange-50: rgba(255, 95, 30, 0.5);
34
+ --orange-30: rgba(255, 95, 30, 0.3);
35
+ --orange-10: rgba(255, 95, 30, 0.1);
36
+
37
+ /* Border style controls - change these to customize all dividers */
38
+ --border-style: solid; /* Options: solid, dashed, dotted */
39
+ /* --border-width: 1px; */
40
+ --border-width: 2px;
41
+
42
+ --dash-length: 5px; /* Only applies when border-style is dashed */
43
+
44
+ /* Semantic color assignments */
45
+ --bg-primary: var(--black);
46
+ --bg-secondary: var(--black-70);
47
+ --bg-tertiary: var(--black-50);
48
+ --text-primary: var(--white);
49
+ --text-secondary: var(--white-70);
50
+ /* --border-color: var(--white-30); */
51
+ --border-color: var(--light-purple-30);
52
+ --accent-primary: var(--purple);
53
+ --accent-secondary: var(--light-purple);
54
+ --accent-hover: var(--light-purple);
55
+ --message-user-bg: var(--purple);
56
+ --message-assistant-bg: var(--black-70);
57
+ --input-bg: var(--black-30);
58
+ --input-border: var(--light-purple-30);
59
+ --input-focus: var(--light-purple);
60
+ }
61
+
62
+ * {
63
+ box-sizing: border-box;
64
+ margin: 0;
65
+ padding: 0;
66
+ }
67
+
68
+ body {
69
+ font-family: 'Söhne', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
70
+ background-color: var(--bg-primary);
71
+ color: var(--text-primary);
72
+ padding: 0;
73
+ margin: 0;
74
+ min-height: 100vh;
75
+ }
76
+
77
+ .app-layout {
78
+ display: flex;
79
+ flex-direction: column;
80
+ height: 100vh;
81
+ overflow: hidden;
82
+ }
83
+
84
+ /* Top Navigation Bar */
85
+ .top-nav {
86
+ position: relative;
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: space-between;
90
+ background-color: var(--bg-secondary);
91
+ border-bottom: var(--border-width) var(--border-style) var(--border-color);
92
+ padding: 24px 24px;
93
+ }
94
+
95
+ .nav-left {
96
+ display: flex;
97
+ align-items: baseline;
98
+ gap: 12px;
99
+ z-index: 1;
100
+ }
101
+
102
+ .nav-center {
103
+ position: absolute;
104
+ left: 50%;
105
+ transform: translateX(-50%);
106
+ display: flex;
107
+ flex-direction: column;
108
+ align-items: center;
109
+ gap: 2px;
110
+ }
111
+
112
+ .nav-title {
113
+ font-size: 22px;
114
+ font-weight: 600;
115
+ color: var(--text-primary);
116
+ }
117
+
118
+ .nav-subtitle {
119
+ font-size: 16px;
120
+ color: var(--text-secondary);
121
+ text-align: center;
122
+ }
123
+
124
+ .nav-logo-img {
125
+ height: 24px;
126
+ width: auto;
127
+ }
128
+
129
+ .nav-logo-link {
130
+ font-size: 28px;
131
+ font-weight: 600;
132
+ color: var(--text-primary);
133
+ text-decoration: none;
134
+ transition: color 0.2s ease;
135
+ display: inline-block;
136
+ line-height: 1;
137
+ }
138
+
139
+ .nav-logo-link:hover {
140
+ color: var(--light-purple);
141
+ }
142
+
143
+ .model-status {
144
+ font-size: 18px;
145
+ font-weight: 500;
146
+ color: var(--text-secondary);
147
+ }
148
+
149
+ .loading-progress {
150
+ width: 100%;
151
+ max-width: 300px;
152
+ margin: 8px 0;
153
+ }
154
+
155
+ .progress-bar {
156
+ width: 100%;
157
+ height: 6px;
158
+ background: var(--white-10);
159
+ border-radius: 3px;
160
+ overflow: hidden;
161
+ }
162
+
163
+ .progress-fill {
164
+ height: 100%;
165
+ background: linear-gradient(90deg, var(--light-purple), var(--orange));
166
+ border-radius: 3px;
167
+ transition: width 0.3s ease;
168
+ }
169
+
170
+ .progress-text {
171
+ font-size: 12px;
172
+ color: var(--text-secondary);
173
+ margin-top: 4px;
174
+ text-align: center;
175
+ }
176
+
177
+ .model-input-wrapper {
178
+ display: flex;
179
+ align-items: center;
180
+ gap: 8px;
181
+ width: 100%;
182
+ }
183
+
184
+ .model-input {
185
+ flex: 1;
186
+ padding: 6px 12px;
187
+ background-color: var(--input-bg);
188
+ border: 1px solid var(--input-border);
189
+ border-radius: 6px;
190
+ font-size: 13px;
191
+ color: var(--text-primary);
192
+ font-family: inherit;
193
+ min-width: 0;
194
+ }
195
+
196
+ select.model-input {
197
+ cursor: pointer;
198
+ appearance: none;
199
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888' d='M6 8L2 4h8z'/%3E%3C/svg%3E");
200
+ background-repeat: no-repeat;
201
+ background-position: right 10px center;
202
+ padding-right: 30px;
203
+ }
204
+
205
+ select.model-input option {
206
+ background-color: var(--bg-primary);
207
+ color: var(--text-primary);
208
+ }
209
+
210
+ .model-input:focus {
211
+ outline: none;
212
+ border-color: var(--input-focus);
213
+ box-shadow: 0 0 0 3px var(--light-purple-10);
214
+ }
215
+
216
+ .model-input:disabled {
217
+ opacity: 0.6;
218
+ cursor: not-allowed;
219
+ }
220
+
221
+ .cache-info {
222
+ font-size: 12px;
223
+ color: var(--text-secondary);
224
+ white-space: nowrap;
225
+ }
226
+
227
+ .container {
228
+ flex: 1;
229
+ background-color: var(--bg-primary);
230
+ display: flex;
231
+ flex-direction: column;
232
+ overflow: hidden;
233
+ min-height: 0;
234
+ }
235
+
236
+ .mode-container {
237
+ display: none;
238
+ flex-direction: column;
239
+ height: 100%;
240
+ overflow: hidden;
241
+ }
242
+
243
+ .mode-container.active {
244
+ display: flex;
245
+ }
246
+
247
+ /* Live Caption Mode Styles */
248
+ #live-caption-mode.active {
249
+ display: flex;
250
+ flex-direction: column;
251
+ width: 100%;
252
+ height: 100%;
253
+ padding: 32px;
254
+ gap: 24px;
255
+ background-color: var(--bg-primary);
256
+ }
257
+
258
+ .live-caption-content {
259
+ flex: 1;
260
+ display: flex;
261
+ gap: 24px;
262
+ min-height: 0;
263
+ }
264
+
265
+ .live-caption-video-section {
266
+ flex: 2;
267
+ display: flex;
268
+ flex-direction: column;
269
+ justify-content: space-between;
270
+ gap: 16px;
271
+ min-width: 0;
272
+ }
273
+
274
+ /* Safari: aspect-ratio on container causes incorrect width calculation */
275
+ .is-safari .live-caption-video-container {
276
+ aspect-ratio: auto;
277
+ width: 100%;
278
+ }
279
+
280
+
281
+ .live-caption-video-container {
282
+ position: relative;
283
+ aspect-ratio: 1;
284
+ height: 100%;
285
+ max-height: calc(100vh - 220px);
286
+ border: 2px solid var(--light-purple-30);
287
+ border-radius: 12px;
288
+ background-color: var(--black-50);
289
+ overflow: hidden;
290
+ display: flex;
291
+ align-items: center;
292
+ justify-content: center;
293
+ }
294
+
295
+ #live-caption-video {
296
+ width: 100%;
297
+ height: 100%;
298
+ object-fit: contain;
299
+ background-color: var(--black);
300
+ transform: scaleX(-1); /* Mirror the video horizontally */
301
+ }
302
+
303
+ /* Capture Overlay (on video) */
304
+ .capture-overlay {
305
+ position: absolute;
306
+ top: 12px;
307
+ right: 12px;
308
+ display: flex;
309
+ flex-direction: column;
310
+ align-items: flex-end;
311
+ gap: 8px;
312
+ padding: 10px 12px;
313
+ background: rgba(0, 0, 0, 0.6);
314
+ backdrop-filter: blur(8px);
315
+ border-radius: 8px;
316
+ z-index: 10;
317
+ min-width: 100px;
318
+ }
319
+
320
+ .capture-overlay .control-btn {
321
+ padding: 8px 16px;
322
+ font-size: 14px;
323
+ width: 100%;
324
+ }
325
+
326
+ .capture-overlay .control-select {
327
+ padding: 6px 10px;
328
+ padding-right: 24px;
329
+ font-size: 14px;
330
+ background-position: right 6px center;
331
+ }
332
+
333
+ .overlay-field {
334
+ display: flex;
335
+ align-items: center;
336
+ gap: 8px;
337
+ }
338
+
339
+ .overlay-label {
340
+ font-size: 14px;
341
+ color: var(--white-70);
342
+ white-space: nowrap;
343
+ }
344
+
345
+ .capture-status {
346
+ display: flex;
347
+ align-items: center;
348
+ justify-content: center;
349
+ gap: 6px;
350
+ }
351
+
352
+ .capture-overlay .status-text {
353
+ font-size: 14px;
354
+ color: var(--white-70);
355
+ }
356
+
357
+ /* Controls Bar */
358
+ .controls-bar {
359
+ display: flex;
360
+ flex-direction: column;
361
+ gap: 10px;
362
+ padding: 12px 16px;
363
+ background-color: var(--black-50);
364
+ border: 1px solid var(--light-purple-30);
365
+ border-radius: 12px;
366
+ }
367
+
368
+ .controls-row {
369
+ display: flex;
370
+ align-items: center;
371
+ justify-content: space-between;
372
+ gap: 16px;
373
+ flex-wrap: wrap;
374
+ }
375
+
376
+ .controls-row.status-row {
377
+ gap: 12px;
378
+ }
379
+
380
+ .control-group {
381
+ display: flex;
382
+ align-items: center;
383
+ gap: 8px;
384
+ }
385
+
386
+ .control-label {
387
+ font-size: 13px;
388
+ color: var(--text-secondary);
389
+ white-space: nowrap;
390
+ }
391
+
392
+ .control-btn {
393
+ padding: 8px 16px;
394
+ background-color: var(--light-purple-30);
395
+ border: 1px solid var(--light-purple-50);
396
+ border-radius: 6px;
397
+ color: var(--text-primary);
398
+ font-size: 13px;
399
+ font-weight: 500;
400
+ cursor: pointer;
401
+ transition: all 0.2s ease;
402
+ white-space: nowrap;
403
+ }
404
+
405
+ .control-btn:hover:not(:disabled) {
406
+ background-color: var(--light-purple-50);
407
+ border-color: var(--light-purple);
408
+ }
409
+
410
+ .control-btn:disabled {
411
+ opacity: 0.5;
412
+ cursor: not-allowed;
413
+ }
414
+
415
+ .control-btn.primary {
416
+ background: linear-gradient(135deg, var(--purple) 0%, var(--light-purple) 100%);
417
+ border: none;
418
+ color: white;
419
+ padding: 8px 20px;
420
+ }
421
+
422
+ .control-btn.primary:hover:not(:disabled) {
423
+ transform: translateY(-1px);
424
+ box-shadow: 0 4px 12px var(--purple-50);
425
+ }
426
+
427
+ .control-btn.primary.stop {
428
+ background: linear-gradient(135deg, var(--orange) 0%, var(--orange-70) 100%);
429
+ }
430
+
431
+ .control-btn.small {
432
+ padding: 6px 12px;
433
+ font-size: 12px;
434
+ }
435
+
436
+ .control-select {
437
+ padding: 8px 12px;
438
+ padding-right: 28px;
439
+ background-color: var(--input-bg);
440
+ border: 1px solid var(--input-border);
441
+ border-radius: 6px;
442
+ color: var(--text-primary);
443
+ font-size: 13px;
444
+ cursor: pointer;
445
+ appearance: none;
446
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888' d='M6 8L2 4h8z'/%3E%3C/svg%3E");
447
+ background-repeat: no-repeat;
448
+ background-position: right 8px center;
449
+ }
450
+
451
+ .control-select:focus {
452
+ outline: none;
453
+ border-color: var(--input-focus);
454
+ }
455
+
456
+ .control-select option {
457
+ background-color: var(--bg-primary);
458
+ color: var(--text-primary);
459
+ }
460
+
461
+ .control-select.model-select {
462
+ min-width: 140px;
463
+ }
464
+
465
+ .status-text {
466
+ font-size: 13px;
467
+ color: var(--text-secondary);
468
+ min-width: 40px;
469
+ }
470
+
471
+ /* Progress Bar (inline in status row) */
472
+ .progress-bar-row {
473
+ flex: 1;
474
+ max-width: 200px;
475
+ height: 4px;
476
+ background: var(--white-10);
477
+ border-radius: 2px;
478
+ overflow: hidden;
479
+ }
480
+
481
+ .progress-bar-row .progress-fill {
482
+ height: 100%;
483
+ background: linear-gradient(90deg, var(--light-purple), var(--orange));
484
+ border-radius: 2px;
485
+ transition: width 0.3s ease;
486
+ }
487
+
488
+ /* Indeterminate progress animation for large downloads */
489
+ .progress-bar-row.indeterminate .progress-fill {
490
+ width: 30% !important;
491
+ animation: indeterminate 1.5s ease-in-out infinite;
492
+ }
493
+
494
+ @keyframes indeterminate {
495
+ 0% { transform: translateX(-100%); }
496
+ 100% { transform: translateX(400%); }
497
+ }
498
+
499
+ .status-indicator {
500
+ width: 10px;
501
+ height: 10px;
502
+ border-radius: 50%;
503
+ background-color: var(--text-secondary);
504
+ }
505
+
506
+ .status-indicator.active {
507
+ background-color: var(--light-purple);
508
+ box-shadow: 0 0 8px var(--light-purple);
509
+ animation: pulse 2s infinite;
510
+ }
511
+
512
+ @keyframes pulse {
513
+ 0%, 100% {
514
+ opacity: 1;
515
+ }
516
+ 50% {
517
+ opacity: 0.5;
518
+ }
519
+ }
520
+
521
+ .live-caption-text-section {
522
+ flex: 1;
523
+ display: flex;
524
+ flex-direction: column;
525
+ gap: 16px;
526
+ min-width: 0;
527
+ padding: 20px;
528
+ background-color: var(--black-50);
529
+ border: 1px solid var(--light-purple-30);
530
+ border-radius: 12px;
531
+ }
532
+
533
+ .caption-section-title {
534
+ font-size: 22px;
535
+ font-weight: 600;
536
+ color: var(--text-primary);
537
+ padding-bottom: 12px;
538
+ border-bottom: 1px solid var(--light-purple-30);
539
+ }
540
+
541
+ .latest-caption {
542
+ font-size: 20px;
543
+ font-weight: 500;
544
+ color: var(--white);
545
+ line-height: 1.4;
546
+ padding: 12px;
547
+ background-color: var(--black-30);
548
+ border-radius: 8px;
549
+ border-left: 3px solid var(--light-purple);
550
+ }
551
+
552
+ .caption-history {
553
+ flex: 1;
554
+ display: flex;
555
+ flex-direction: column;
556
+ gap: 8px;
557
+ overflow-y: auto;
558
+ }
559
+
560
+ .caption-history-item {
561
+ display: flex;
562
+ gap: 12px;
563
+ padding: 12px 16px;
564
+ background-color: var(--black-30);
565
+ border-radius: 8px;
566
+ border: 1px solid var(--light-purple-30);
567
+ border-left: 3px solid transparent;
568
+ transition: opacity 0.3s ease, border-color 0.3s ease;
569
+ }
570
+
571
+ .caption-history-item.latest {
572
+ border-left-color: var(--light-purple);
573
+ background-color: var(--black-50);
574
+ }
575
+
576
+ .caption-timestamp {
577
+ font-size: 11px;
578
+ color: var(--text-secondary);
579
+ flex-shrink: 0;
580
+ font-family: 'JetBrains Mono', monospace;
581
+ opacity: 0.7;
582
+ }
583
+
584
+ .caption-text {
585
+ font-size: 14px;
586
+ color: var(--text-primary);
587
+ line-height: 1.4;
588
+ }
589
+
590
+ /* Responsive Design */
591
+ @media (max-width: 1024px) {
592
+ .live-caption-content {
593
+ flex-direction: column;
594
+ }
595
+
596
+ .live-caption-video-section,
597
+ .live-caption-text-section {
598
+ flex: none;
599
+ width: 100%;
600
+ }
601
+
602
+ .live-caption-text-section {
603
+ max-height: 300px;
604
+ }
605
+ }
606
+
607
+ @media (max-width: 768px) {
608
+ .top-nav {
609
+ flex-direction: column;
610
+ align-items: flex-start;
611
+ gap: 16px;
612
+ padding: 12px 16px;
613
+ }
614
+
615
+ .nav-left,
616
+ .nav-center {
617
+ width: 100%;
618
+ }
619
+
620
+ .nav-center {
621
+ position: static;
622
+ transform: none;
623
+ align-items: flex-start;
624
+ }
625
+
626
+ .nav-subtitle {
627
+ text-align: left;
628
+ font-size: 14px;
629
+ }
630
+
631
+ .model-status {
632
+ max-width: 100%;
633
+ }
634
+
635
+ .model-input-wrapper {
636
+ width: 100%;
637
+ }
638
+
639
+ #live-caption-mode.active {
640
+ padding: 16px;
641
+ gap: 16px;
642
+ }
643
+
644
+ .live-caption-video-container {
645
+ min-height: 200px;
646
+ }
647
+
648
+ .live-caption-text-section {
649
+ padding: 16px;
650
+ }
651
+
652
+ .latest-caption {
653
+ font-size: 20px;
654
+ padding: 12px;
655
+ }
656
+
657
+ /* Controls bar mobile layout */
658
+ .controls-bar {
659
+ padding: 12px;
660
+ }
661
+
662
+ .controls-row {
663
+ flex-direction: column;
664
+ align-items: stretch;
665
+ gap: 12px;
666
+ }
667
+
668
+ .control-group {
669
+ flex-wrap: wrap;
670
+ width: 100%;
671
+ }
672
+
673
+ .control-group.model-group {
674
+ flex-direction: column;
675
+ align-items: stretch;
676
+ gap: 10px;
677
+ }
678
+
679
+ .control-group.model-group .control-label {
680
+ margin-bottom: 4px;
681
+ }
682
+
683
+ .control-group.model-group .control-select {
684
+ width: 100%;
685
+ min-width: auto;
686
+ }
687
+
688
+ .control-group.model-group .control-btn {
689
+ width: 100%;
690
+ padding: 12px 16px;
691
+ }
692
+
693
+ .control-group.cache-group {
694
+ flex-direction: row;
695
+ justify-content: space-between;
696
+ align-items: center;
697
+ }
698
+
699
+ /* Allow scrolling on mobile */
700
+ .app-layout {
701
+ overflow-y: auto;
702
+ overflow-x: hidden;
703
+ }
704
+
705
+ .container {
706
+ overflow-y: auto;
707
+ overflow-x: hidden;
708
+ }
709
+
710
+ #live-caption-mode.active {
711
+ overflow-y: auto;
712
+ min-height: auto;
713
+ height: auto;
714
+ }
715
+
716
+ .live-caption-content {
717
+ min-height: auto;
718
+ }
719
+
720
+ .live-caption-video-section {
721
+ flex-shrink: 0;
722
+ }
723
+ }
724
+
725
+
726
+ /* Smooth transitions */
727
+ .mode-container {
728
+ transition: opacity 0.2s ease;
729
+ }
730
+
731
+ /* Scrollbar styling for webkit browsers */
732
+ .caption-history::-webkit-scrollbar {
733
+ width: 6px;
734
+ }
735
+
736
+ .caption-history::-webkit-scrollbar-track {
737
+ background: var(--black-30);
738
+ }
739
+
740
+ .caption-history::-webkit-scrollbar-thumb {
741
+ background: var(--light-purple-50);
742
+ border-radius: 3px;
743
+ }
744
+
745
+ .caption-history::-webkit-scrollbar-thumb:hover {
746
+ background: var(--light-purple);
747
+ }
748
+
749
+ /* Loading states */
750
+ @keyframes shimmer {
751
+ 0% {
752
+ background-position: -1000px 0;
753
+ }
754
+ 100% {
755
+ background-position: 1000px 0;
756
+ }
757
+ }
758
+
759
+ .loading {
760
+ background: linear-gradient(
761
+ 90deg,
762
+ var(--bg-tertiary) 0%,
763
+ var(--bg-secondary) 50%,
764
+ var(--bg-tertiary) 100%
765
+ );
766
+ background-size: 1000px 100%;
767
+ animation: shimmer 2s infinite;
768
+ }
769
+
770
+ /* Focus visible for accessibility */
771
+ button:focus-visible,
772
+ input:focus-visible {
773
+ outline: 2px solid var(--light-purple);
774
+ outline-offset: 2px;
775
+ }
776
+
777
+ /* Ensure proper text rendering */
778
+ body {
779
+ -webkit-font-smoothing: antialiased;
780
+ -moz-osx-font-smoothing: grayscale;
781
+ text-rendering: optimizeLegibility;
782
+ }
783
+
784
+
785
+ /* High contrast improvements */
786
+ @media (prefers-contrast: high) {
787
+ button:not(:disabled) {
788
+ border: 2px solid var(--light-purple);
789
+ }
790
+ }
791
+
792
+ /* ===================================
793
+ LOADING SCREEN / WELCOME PAGE
794
+ =================================== */
795
+
796
+ .loading-screen {
797
+ position: fixed;
798
+ top: 0;
799
+ left: 0;
800
+ width: 100%;
801
+ height: 100%;
802
+ background-color: var(--black);
803
+ z-index: 10000;
804
+ display: flex;
805
+ align-items: center;
806
+ justify-content: center;
807
+ overflow-y: auto;
808
+ overflow-x: hidden;
809
+ }
810
+
811
+ .loading-screen.hidden {
812
+ display: none;
813
+ }
814
+
815
+ .loading-canvas {
816
+ position: absolute;
817
+ top: 0;
818
+ left: 0;
819
+ width: 100%;
820
+ height: 100%;
821
+ z-index: 1;
822
+ }
823
+
824
+ .loading-vignette {
825
+ position: absolute;
826
+ top: 0;
827
+ left: 0;
828
+ width: 100%;
829
+ height: 100%;
830
+ background: radial-gradient(ellipse at center, transparent 0%, rgba(0, 0, 0, 0.7) 100%);
831
+ z-index: 2;
832
+ }
833
+
834
+ .loading-content {
835
+ position: relative;
836
+ z-index: 3;
837
+ display: flex;
838
+ flex-direction: column;
839
+ align-items: center;
840
+ justify-content: center;
841
+ padding: 2rem;
842
+ max-width: 800px;
843
+ width: 100%;
844
+ text-align: center;
845
+ color: var(--white);
846
+ margin: auto;
847
+ }
848
+
849
+ .loading-header {
850
+ margin-bottom: 2rem;
851
+ }
852
+
853
+ .loading-logo {
854
+ height: 60px;
855
+ width: auto;
856
+ margin-bottom: 1rem;
857
+ }
858
+
859
+ .loading-title-section {
860
+ margin-bottom: 2rem;
861
+ }
862
+
863
+ .loading-title {
864
+ font-size: 2.5rem;
865
+ font-weight: 700;
866
+ color: var(--white);
867
+ margin: 0 0 0.5rem 0;
868
+ text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
869
+ }
870
+
871
+ .loading-subtitle {
872
+ font-size: 1.25rem;
873
+ color: var(--white-70);
874
+ margin: 0;
875
+ font-weight: 400;
876
+ }
877
+
878
+ .loading-description {
879
+ margin-bottom: 3rem;
880
+ max-width: 600px;
881
+ }
882
+
883
+ .loading-description p {
884
+ font-size: 1rem;
885
+ color: var(--white-70);
886
+ line-height: 1.6;
887
+ margin: 0 0 1rem 0;
888
+ }
889
+
890
+ .loading-description p:last-child {
891
+ margin-bottom: 0;
892
+ }
893
+
894
+ .loading-action-section {
895
+ margin-bottom: 2rem;
896
+ }
897
+
898
+ .loading-explore-button {
899
+ padding: 16px 48px;
900
+ background: linear-gradient(135deg, var(--purple) 0%, var(--light-purple) 100%);
901
+ color: var(--white);
902
+ border: none;
903
+ border-radius: 12px;
904
+ font-size: 1.125rem;
905
+ font-weight: 600;
906
+ cursor: pointer;
907
+ transition: all 0.3s ease;
908
+ box-shadow: 0 4px 16px var(--purple-50);
909
+ display: inline-flex;
910
+ align-items: center;
911
+ gap: 12px;
912
+ min-width: 200px;
913
+ justify-content: center;
914
+ }
915
+
916
+ .loading-explore-button:hover:not(:disabled) {
917
+ transform: translateY(-2px);
918
+ box-shadow: 0 6px 24px var(--purple-70);
919
+ background: linear-gradient(135deg, var(--light-purple) 0%, var(--purple) 100%);
920
+ }
921
+
922
+ .loading-explore-button:active:not(:disabled) {
923
+ transform: translateY(0);
924
+ }
925
+
926
+ .loading-explore-button:disabled {
927
+ opacity: 0.7;
928
+ cursor: not-allowed;
929
+ }
930
+
931
+ .loading-spinner {
932
+ display: inline-block;
933
+ width: 20px;
934
+ height: 20px;
935
+ border: 3px solid var(--white-30);
936
+ border-top-color: var(--white);
937
+ border-radius: 50%;
938
+ animation: spin 0.8s linear infinite;
939
+ }
940
+
941
+ @keyframes spin {
942
+ to {
943
+ transform: rotate(360deg);
944
+ }
945
+ }
946
+
947
+ .loading-progress-text {
948
+ font-size: 0.875rem;
949
+ color: var(--white-70);
950
+ font-weight: 500;
951
+ }
952
+
953
+ .loading-error {
954
+ margin-top: 2rem;
955
+ padding: 1.5rem;
956
+ background-color: var(--black-70);
957
+ border: 1px solid var(--orange-50);
958
+ border-radius: 8px;
959
+ max-width: 500px;
960
+ }
961
+
962
+ .loading-error p {
963
+ color: var(--orange);
964
+ margin: 0 0 1rem 0;
965
+ font-size: 0.95rem;
966
+ }
967
+
968
+ .loading-retry-button {
969
+ padding: 10px 24px;
970
+ background-color: var(--orange);
971
+ color: var(--white);
972
+ border: none;
973
+ border-radius: 8px;
974
+ font-size: 0.95rem;
975
+ font-weight: 500;
976
+ cursor: pointer;
977
+ transition: all 0.2s ease;
978
+ }
979
+
980
+ .loading-retry-button:hover {
981
+ background-color: var(--orange-70);
982
+ transform: translateY(-1px);
983
+ }
984
+
985
+ .hidden {
986
+ display: none !important;
987
+ }
988
+
989
+ @media (max-width: 768px) {
990
+ .loading-screen {
991
+ align-items: flex-start;
992
+ }
993
+
994
+ .loading-content {
995
+ padding: 2rem 1.5rem;
996
+ min-height: 100%;
997
+ justify-content: flex-start;
998
+ padding-top: 3rem;
999
+ }
1000
+
1001
+ .loading-title {
1002
+ font-size: 1.75rem;
1003
+ }
1004
+
1005
+ .loading-subtitle {
1006
+ font-size: 1rem;
1007
+ }
1008
+
1009
+ .loading-description p {
1010
+ font-size: 0.9rem;
1011
+ }
1012
+
1013
+ .loading-explore-button {
1014
+ padding: 14px 36px;
1015
+ font-size: 1rem;
1016
+ min-width: 180px;
1017
+ }
1018
+ }
1019
+
1020
+ /* Mobile Warning */
1021
+ .mobile-warning {
1022
+ margin-bottom: 1.5rem;
1023
+ padding: 1rem 1.5rem;
1024
+ background-color: rgba(255, 95, 30, 0.15);
1025
+ border: 1px solid var(--orange-50);
1026
+ border-radius: 8px;
1027
+ max-width: 400px;
1028
+ }
1029
+
1030
+ .mobile-warning-title {
1031
+ color: var(--orange);
1032
+ font-weight: 600;
1033
+ font-size: 1rem;
1034
+ margin-bottom: 0.5rem;
1035
+ text-align: center;
1036
+ display: flex;
1037
+ align-items: center;
1038
+ justify-content: center;
1039
+ gap: 6px;
1040
+ }
1041
+
1042
+ .mobile-warning-title svg {
1043
+ flex-shrink: 0;
1044
+ }
1045
+
1046
+ .mobile-warning p {
1047
+ color: var(--white-70);
1048
+ font-size: 0.85rem;
1049
+ line-height: 1.5;
1050
+ margin: 0 0 0.5rem 0;
1051
+ text-align: center;
1052
+ }
1053
+
1054
+ .mobile-warning p:last-child {
1055
+ margin-bottom: 0;
1056
+ }
1057
+
1058
+ /* Safari Warning */
1059
+ .safari-warning {
1060
+ margin-bottom: 1.5rem;
1061
+ padding: 1rem 1.5rem;
1062
+ background-color: rgba(205, 130, 240, 0.15);
1063
+ border: 1px solid var(--light-purple-50);
1064
+ border-radius: 8px;
1065
+ max-width: 500px;
1066
+ }
1067
+
1068
+ .safari-warning-title {
1069
+ color: var(--light-purple);
1070
+ font-weight: 600;
1071
+ font-size: 1rem;
1072
+ margin-bottom: 0.5rem;
1073
+ text-align: center;
1074
+ display: flex;
1075
+ align-items: center;
1076
+ justify-content: center;
1077
+ gap: 6px;
1078
+ }
1079
+
1080
+ .safari-warning-title svg {
1081
+ flex-shrink: 0;
1082
+ }
1083
+
1084
+ .safari-warning p {
1085
+ color: var(--white-70);
1086
+ font-size: 0.85rem;
1087
+ line-height: 1.5;
1088
+ margin: 0 0 0.5rem 0;
1089
+ text-align: center;
1090
+ }
1091
+
1092
+ .safari-warning p:last-child {
1093
+ margin-bottom: 0;
1094
+ }
1095
+
1096
+ .safari-warning a {
1097
+ color: var(--light-purple);
1098
+ text-decoration: underline;
1099
+ }
1100
+
1101
+ .safari-warning a:hover {
1102
+ color: var(--white);
1103
+ }
ui.js ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * UI rendering and DOM manipulation
3
+ */
4
+
5
+ // WebGPU/Model control getters
6
+ function getModelSelect() {
7
+ return document.getElementById('model-select');
8
+ }
9
+
10
+ function getLoadModelBtn() {
11
+ return document.getElementById('load-model-btn');
12
+ }
13
+
14
+ function getLoadingProgress() {
15
+ return document.getElementById('loading-progress');
16
+ }
17
+
18
+ function getProgressFill() {
19
+ return document.getElementById('progress-fill');
20
+ }
21
+
22
+ function getProgressText() {
23
+ return document.getElementById('progress-text');
24
+ }
25
+
26
+ function getModelStatus() {
27
+ return document.getElementById('model-status');
28
+ }
29
+
30
+ function getWebGPUStatus() {
31
+ return document.getElementById('webgpu-status');
32
+ }
33
+
34
+ function getModelSection() {
35
+ return document.getElementById('model-section');
36
+ }
37
+
38
+ function getReloadModelBtn() {
39
+ return document.getElementById('reload-model-btn');
40
+ }
41
+
42
+ function getClearCacheBtn() {
43
+ return document.getElementById('clear-cache-btn');
44
+ }
45
+
46
+ function getCacheInfo() {
47
+ return document.getElementById('cache-info');
48
+ }
49
+
50
+ /**
51
+ * Populate model selector with available models
52
+ * @param {Array} models - Array of model configurations
53
+ */
54
+ export function populateModelSelector(models) {
55
+ const modelSelect = getModelSelect();
56
+ if (!modelSelect) return;
57
+
58
+ modelSelect.innerHTML = '';
59
+ models.forEach(model => {
60
+ const option = document.createElement('option');
61
+ option.value = model.id;
62
+ const noteText = model.note ? ` - ${model.note}` : '';
63
+ option.textContent = `${model.label} (${model.size}${noteText})`;
64
+ modelSelect.appendChild(option);
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Show/hide model section based on inference mode
70
+ * @param {boolean} show - Whether to show the model section
71
+ */
72
+ export function toggleModelSection(show) {
73
+ const modelSection = getModelSection();
74
+ if (modelSection) {
75
+ if (show) {
76
+ modelSection.classList.remove('hidden');
77
+ } else {
78
+ modelSection.classList.add('hidden');
79
+ }
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Update loading progress
85
+ * @param {number} percent - Progress percentage (0-100), or -1 for indeterminate
86
+ */
87
+ export function updateLoadingProgress(percent) {
88
+ const progressFill = getProgressFill();
89
+ const progressBar = getLoadingProgress();
90
+
91
+ // Handle indeterminate state (percent < 0)
92
+ if (progressBar) {
93
+ if (percent < 0) {
94
+ progressBar.classList.add('indeterminate');
95
+ } else {
96
+ progressBar.classList.remove('indeterminate');
97
+ }
98
+ }
99
+
100
+ if (progressFill) {
101
+ progressFill.style.width = percent < 0 ? '30%' : `${percent}%`;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Show/hide loading progress
107
+ * @param {boolean} show - Whether to show the progress bar
108
+ */
109
+ export function showLoadingProgress(show) {
110
+ const loadingProgress = getLoadingProgress();
111
+ if (loadingProgress) {
112
+ loadingProgress.style.display = show ? 'block' : 'none';
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Show/hide the model input wrapper (dropdown + load button)
118
+ * @param {boolean} show - Whether to show the input wrapper
119
+ */
120
+ export function showModelInputWrapper(show) {
121
+ const wrapper = document.querySelector('.model-input-wrapper');
122
+ if (wrapper) {
123
+ wrapper.style.display = show ? 'flex' : 'none';
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Update model status display
129
+ * @param {string} message - Status message
130
+ * @param {string} type - Status type ('success', 'error', 'loading', or '')
131
+ */
132
+ export function updateModelStatus(message, type = '') {
133
+ const modelStatus = getModelStatus();
134
+ if (modelStatus) {
135
+ modelStatus.textContent = message;
136
+ modelStatus.className = 'model-status';
137
+ if (type) {
138
+ modelStatus.classList.add(type);
139
+ }
140
+ modelStatus.style.display = message ? 'block' : 'none';
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Update WebGPU status display
146
+ * @param {string} message - Status message
147
+ * @param {boolean} available - Whether WebGPU is available
148
+ */
149
+ export function updateWebGPUStatus(message, available) {
150
+ const webgpuStatus = getWebGPUStatus();
151
+ if (webgpuStatus) {
152
+ webgpuStatus.textContent = message;
153
+ webgpuStatus.className = 'webgpu-status';
154
+ if (available) {
155
+ webgpuStatus.classList.add('available');
156
+ } else {
157
+ webgpuStatus.classList.add('unavailable');
158
+ }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Enable/disable model loading button
164
+ * @param {boolean} enabled - Whether to enable the button
165
+ */
166
+ export function setLoadModelButtonEnabled(enabled) {
167
+ const loadModelBtn = getLoadModelBtn();
168
+ if (loadModelBtn) {
169
+ loadModelBtn.disabled = !enabled;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Get selected model ID
175
+ * @returns {string|null}
176
+ */
177
+ export function getSelectedModelId() {
178
+ const modelSelect = getModelSelect();
179
+ return modelSelect ? modelSelect.value : null;
180
+ }
181
+
182
+ /**
183
+ * Update button states based on model load status
184
+ * @param {boolean} modelLoaded - Whether the model is loaded
185
+ */
186
+ export function updateButtonStates(modelLoaded) {
187
+ const tooltipText = modelLoaded ? '' : 'Load a model first';
188
+
189
+ // Update live caption button
190
+ const liveCaptionBtn = document.getElementById('start-live-caption-btn');
191
+ if (liveCaptionBtn) {
192
+ liveCaptionBtn.disabled = !modelLoaded;
193
+ liveCaptionBtn.title = tooltipText;
194
+ // Update button text based on model state
195
+ if (!modelLoaded) {
196
+ liveCaptionBtn.textContent = 'Load Model Below';
197
+ } else {
198
+ liveCaptionBtn.textContent = 'Start';
199
+ }
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Set up event listeners (simplified - only for model loading)
205
+ * @param {Function} onSend - Not used (kept for compatibility)
206
+ * @param {Function} onLoadModel - Callback for load model button
207
+ * @param {Function} onReloadModel - Callback for reload model button
208
+ */
209
+ export function setupEventListeners(onSend, onLoadModel, onReloadModel) {
210
+ // Load model button
211
+ const loadModelBtn = getLoadModelBtn();
212
+ if (loadModelBtn && onLoadModel) {
213
+ loadModelBtn.addEventListener('click', onLoadModel);
214
+ }
215
+
216
+ // Reload model button
217
+ const reloadModelBtn = getReloadModelBtn();
218
+ if (reloadModelBtn && onReloadModel) {
219
+ reloadModelBtn.addEventListener('click', onReloadModel);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Update cache info display
225
+ * @param {number} usedBytes - Bytes used by cache
226
+ */
227
+ export function updateCacheInfo(usedBytes) {
228
+ const cacheInfoEl = getCacheInfo();
229
+ const clearCacheBtn = getClearCacheBtn();
230
+
231
+ if (!cacheInfoEl) return;
232
+
233
+ if (usedBytes > 0) {
234
+ const usedMB = usedBytes / 1024 / 1024;
235
+ if (usedMB >= 1000) {
236
+ cacheInfoEl.textContent = `${(usedMB / 1024).toFixed(1)} GB cached`;
237
+ } else if (usedMB >= 1) {
238
+ cacheInfoEl.textContent = `${usedMB.toFixed(0)} MB cached`;
239
+ } else {
240
+ cacheInfoEl.textContent = 'No models cached';
241
+ }
242
+ if (clearCacheBtn) {
243
+ clearCacheBtn.disabled = usedMB < 1;
244
+ }
245
+ } else {
246
+ cacheInfoEl.textContent = 'No models cached';
247
+ if (clearCacheBtn) {
248
+ clearCacheBtn.disabled = true;
249
+ }
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Set up clear cache button handler
255
+ * @param {Function} onClearCache - Callback for clear cache button
256
+ */
257
+ export function setupClearCacheHandler(onClearCache) {
258
+ const clearCacheBtn = getClearCacheBtn();
259
+ if (clearCacheBtn && onClearCache) {
260
+ clearCacheBtn.addEventListener('click', onClearCache);
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Set clear cache button text
266
+ * @param {string} text - Button text
267
+ */
268
+ export function setClearCacheButtonText(text) {
269
+ const clearCacheBtn = getClearCacheBtn();
270
+ if (clearCacheBtn) {
271
+ clearCacheBtn.textContent = text;
272
+ }
273
+ }
vite.config.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite';
2
+
3
+ export default defineConfig({
4
+ server: {
5
+ port: 3000,
6
+ headers: {
7
+ // Required for SharedArrayBuffer (ONNX Runtime threading)
8
+ 'Cross-Origin-Opener-Policy': 'same-origin',
9
+ 'Cross-Origin-Embedder-Policy': 'require-corp',
10
+ },
11
+ },
12
+
13
+ preview: {
14
+ headers: {
15
+ 'Cross-Origin-Opener-Policy': 'same-origin',
16
+ 'Cross-Origin-Embedder-Policy': 'require-corp',
17
+ },
18
+ },
19
+
20
+ optimizeDeps: {
21
+ exclude: ['@huggingface/transformers', 'onnxruntime-web'],
22
+ },
23
+
24
+ build: {
25
+ target: 'esnext',
26
+ },
27
+ });
vl-model.js ADDED
@@ -0,0 +1,974 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * LFM2-VL Model Runner for ONNX Runtime Web
3
+ *
4
+ * Runs VL model inference using three ONNX models:
5
+ * 1. embed_tokens.onnx - Text token embeddings
6
+ * 2. vision_encoder.onnx - Image embeddings from patches
7
+ * 3. decoder_model_merged.onnx - Autoregressive decoder with conv state cache
8
+ */
9
+
10
+ import * as ort from 'onnxruntime-web';
11
+ import { AutoTokenizer, env } from '@huggingface/transformers';
12
+ import { processImage, loadImage } from './vl-processor.js';
13
+
14
+ // Debug logging - set to false for production, toggle via setDebug(true) in console
15
+ let DEBUG = false;
16
+ export function setDebug(value) { DEBUG = value; console.log(`Debug logging ${value ? 'enabled' : 'disabled'}`); }
17
+ const log = (...args) => { if (DEBUG) console.log(...args); };
18
+
19
+ /**
20
+ * Convert float32 to float16 (IEEE 754 half-precision)
21
+ * @param {number} float32 - Float32 value
22
+ * @returns {number} - Float16 value as uint16
23
+ */
24
+ function float32ToFloat16(float32) {
25
+ const view = new DataView(new ArrayBuffer(4));
26
+ view.setFloat32(0, float32, true);
27
+ const f32 = view.getUint32(0, true);
28
+
29
+ const sign = (f32 >> 31) & 0x1;
30
+ const exp = (f32 >> 23) & 0xff;
31
+ const frac = f32 & 0x7fffff;
32
+
33
+ let f16;
34
+ if (exp === 0) {
35
+ // Zero or denormal
36
+ f16 = (sign << 15) | (frac >> 13);
37
+ } else if (exp === 0xff) {
38
+ // Inf or NaN
39
+ f16 = (sign << 15) | 0x7c00 | (frac ? (frac >> 13) : 0);
40
+ } else {
41
+ // Normalized
42
+ const newExp = exp - 127 + 15;
43
+ if (newExp >= 31) {
44
+ // Overflow to infinity
45
+ f16 = (sign << 15) | 0x7c00;
46
+ } else if (newExp <= 0) {
47
+ // Underflow to zero
48
+ f16 = (sign << 15);
49
+ } else {
50
+ f16 = (sign << 15) | (newExp << 10) | (frac >> 13);
51
+ }
52
+ }
53
+ return f16;
54
+ }
55
+
56
+ /**
57
+ * Convert Float32Array to float16 Uint16Array
58
+ * @param {Float32Array} float32Array
59
+ * @returns {Uint16Array}
60
+ */
61
+ function convertToFloat16(float32Array) {
62
+ const result = new Uint16Array(float32Array.length);
63
+ for (let i = 0; i < float32Array.length; i++) {
64
+ result[i] = float32ToFloat16(float32Array[i]);
65
+ }
66
+ return result;
67
+ }
68
+
69
+ /**
70
+ * Convert a float32 tensor to float16 tensor
71
+ * @param {ort.Tensor} tensor - Float32 tensor
72
+ * @returns {ort.Tensor} - Float16 tensor
73
+ */
74
+ function tensorToFloat16(tensor) {
75
+ const float16Data = convertToFloat16(tensor.data);
76
+ return new ort.Tensor('float16', float16Data, tensor.dims);
77
+ }
78
+
79
+ // Cache configuration
80
+ const CACHE_NAME = 'onnx-models-v1';
81
+
82
+ // Threshold for URL-based ONNX loading (files too large for JS memory)
83
+ // Set to 2GB - files larger than this will stream instead of loading into memory
84
+ const LARGE_FILE_THRESHOLD = 2 * 1024 * 1024 * 1024; // 2GB
85
+
86
+ /**
87
+ * Fetch with streaming progress tracking
88
+ * @param {string} url - URL to fetch
89
+ * @param {object} options - Fetch options
90
+ * @param {function} onProgress - Progress callback (received, total) => void
91
+ * @returns {Promise<Response>} - Response with complete body
92
+ */
93
+ async function fetchWithProgress(url, options = {}, onProgress) {
94
+ const response = await fetch(url, options);
95
+ if (!response.ok) {
96
+ throw new Error(`Fetch failed: ${response.status}`);
97
+ }
98
+
99
+ const contentLength = parseInt(response.headers.get('content-length') || '0', 10);
100
+ if (!contentLength || !onProgress) {
101
+ // No size info or no callback - return as-is
102
+ return response;
103
+ }
104
+
105
+ const reader = response.body.getReader();
106
+ const chunks = [];
107
+ let received = 0;
108
+
109
+ while (true) {
110
+ const { done, value } = await reader.read();
111
+ if (done) break;
112
+ chunks.push(value);
113
+ received += value.length;
114
+ onProgress(received, contentLength);
115
+ }
116
+
117
+ // Combine chunks into single buffer
118
+ const buffer = new Uint8Array(received);
119
+ let offset = 0;
120
+ for (const chunk of chunks) {
121
+ buffer.set(chunk, offset);
122
+ offset += chunk.length;
123
+ }
124
+
125
+ // Create new Response with fresh Headers for Cache API compatibility
126
+ // Using the original headers object from a consumed response can cause issues
127
+ return new Response(new Blob([buffer]), {
128
+ status: response.status,
129
+ headers: new Headers(response.headers),
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Fetch with caching support using Cache API
135
+ * @param {string} url - URL to fetch
136
+ * @param {object} options - Fetch options
137
+ * @param {function} onProgress - Optional progress callback (received, total) => void
138
+ * @returns {Promise<Response>} - Response (from cache or network)
139
+ */
140
+ async function fetchWithCache(url, options = {}, onProgress = null) {
141
+ // Skip caching for local files
142
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
143
+ return fetch(url, options);
144
+ }
145
+
146
+ const fileName = url.split('/').pop();
147
+
148
+ // 1. Try cache read with validation
149
+ try {
150
+ const cache = await caches.open(CACHE_NAME);
151
+ const cached = await cache.match(url);
152
+ if (cached) {
153
+ // Validate by reading body - catches corrupted entries from failed cache.put()
154
+ try {
155
+ const buffer = await cached.clone().arrayBuffer();
156
+ log(`[Cache HIT] ${fileName} (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB)`);
157
+ // Return a new Response with the validated buffer
158
+ return new Response(buffer, {
159
+ status: cached.status,
160
+ statusText: cached.statusText,
161
+ headers: cached.headers,
162
+ });
163
+ } catch (bodyError) {
164
+ // Corrupted cache entry - delete it and re-fetch
165
+ log(`[Cache CORRUPT] ${fileName} - deleting and re-fetching`);
166
+ await cache.delete(url);
167
+ }
168
+ }
169
+ } catch (e) {
170
+ log(`[Cache ERROR] ${e.message}`);
171
+ }
172
+
173
+ // 2. Fetch from network with progress tracking
174
+ log(`[Network] Fetching ${fileName}...`);
175
+ const response = await fetchWithProgress(url, options, onProgress);
176
+
177
+ // 3. Try to cache successful response (fire-and-forget)
178
+ if (response.ok) {
179
+ tryCacheResponse(url, response.clone());
180
+ }
181
+
182
+ return response;
183
+ }
184
+
185
+ /**
186
+ * Try to cache a response (non-blocking, best-effort)
187
+ * @param {string} url - URL to cache
188
+ * @param {Response} response - Response to cache
189
+ */
190
+ async function tryCacheResponse(url, response) {
191
+ try {
192
+ // Check available space before caching
193
+ if (navigator.storage?.estimate) {
194
+ const { usage = 0, quota = 0 } = await navigator.storage.estimate();
195
+ const available = quota - usage;
196
+ const responseSize = parseInt(response.headers.get('content-length') || '0', 10);
197
+
198
+ // Skip if we don't have space for this file + 100MB buffer
199
+ const BUFFER = 100 * 1024 * 1024;
200
+ if (responseSize > 0 && available < responseSize + BUFFER) {
201
+ log(`[Cache SKIP] Not enough space (need ${((responseSize + BUFFER) / 1e9).toFixed(2)} GB, have ${(available / 1e9).toFixed(2)} GB)`);
202
+ return;
203
+ }
204
+ }
205
+
206
+ const cache = await caches.open(CACHE_NAME);
207
+ await cache.put(url, response);
208
+ log(`[Cached] ${url.split('/').pop()}`);
209
+ } catch (e) {
210
+ // Caching failed, but download succeeded - that's fine
211
+ console.warn(`[Cache WRITE ERROR] ${url.split('/').pop()}:`, e.name, e.message, e);
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Clear the model cache
217
+ * @returns {Promise<boolean>} - True if cache was deleted
218
+ */
219
+ export async function clearModelCache() {
220
+ const deleted = await caches.delete(CACHE_NAME);
221
+ log(deleted ? 'Model cache cleared' : 'No cache to clear');
222
+ return deleted;
223
+ }
224
+
225
+ /**
226
+ * Get cache storage usage info (specifically for model cache)
227
+ * @returns {Promise<{used: number, available: number}|null>}
228
+ */
229
+ export async function getCacheInfo() {
230
+ try {
231
+ // Calculate actual size of just the model cache
232
+ const cache = await caches.open(CACHE_NAME);
233
+ const keys = await cache.keys();
234
+
235
+ let totalSize = 0;
236
+ for (const request of keys) {
237
+ const response = await cache.match(request);
238
+ if (response) {
239
+ // Get the response body as blob to measure size
240
+ const blob = await response.clone().blob();
241
+ totalSize += blob.size;
242
+ }
243
+ }
244
+
245
+ // Get quota info for available space
246
+ let available = 0;
247
+ if ('storage' in navigator && 'estimate' in navigator.storage) {
248
+ const estimate = await navigator.storage.estimate();
249
+ available = estimate.quota || 0;
250
+ }
251
+
252
+ return {
253
+ used: totalSize,
254
+ available: available,
255
+ };
256
+ } catch (e) {
257
+ console.warn('Error getting cache info:', e);
258
+ return null;
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Load tokenizer from model path (local or S3)
264
+ * @param {string} modelPath - Path to model directory (local or S3 URL)
265
+ * @returns {Promise<{tokenizer: object, specialTokens: object}>} - Tokenizer instance and special token IDs
266
+ */
267
+ async function loadTokenizerFromPath(modelPath) {
268
+ const isRemote = modelPath.startsWith('http://') || modelPath.startsWith('https://');
269
+ log(`Loading tokenizer from ${isRemote ? 'remote' : 'local'}: ${modelPath}`);
270
+
271
+ const fetchOptions = isRemote ? { mode: 'cors', credentials: 'omit' } : {};
272
+
273
+ // Fetch tokenizer files (with caching)
274
+ const [tokenizerResponse, configResponse] = await Promise.all([
275
+ fetchWithCache(`${modelPath}/tokenizer.json`, fetchOptions),
276
+ fetchWithCache(`${modelPath}/tokenizer_config.json`, fetchOptions),
277
+ ]);
278
+
279
+ if (!tokenizerResponse.ok) {
280
+ throw new Error(`Failed to fetch tokenizer.json: ${tokenizerResponse.status}`);
281
+ }
282
+ if (!configResponse.ok) {
283
+ throw new Error(`Failed to fetch tokenizer_config.json: ${configResponse.status}`);
284
+ }
285
+
286
+ const tokenizerJSON = await tokenizerResponse.text();
287
+ const configJSON = await configResponse.text();
288
+
289
+ log('Tokenizer files fetched, creating tokenizer...');
290
+
291
+ // Parse tokenizer.json to extract special token IDs from added_tokens
292
+ const tokenizerData = JSON.parse(tokenizerJSON);
293
+ const specialTokens = {};
294
+
295
+ if (tokenizerData.added_tokens) {
296
+ for (const token of tokenizerData.added_tokens) {
297
+ specialTokens[token.content] = token.id;
298
+ }
299
+ log('Found special tokens:', Object.keys(specialTokens).length);
300
+ }
301
+
302
+ // Create a unique fake model ID
303
+ const fakeModelId = `tokenizer-${Date.now()}`;
304
+
305
+ // Cache of files to serve
306
+ const fileCache = {
307
+ 'tokenizer.json': tokenizerJSON,
308
+ 'tokenizer_config.json': configJSON,
309
+ };
310
+
311
+ // Intercept fetch to serve our cached files
312
+ const originalFetch = globalThis.fetch;
313
+ globalThis.fetch = async (input, init) => {
314
+ const url = typeof input === 'string' ? input : input.url;
315
+
316
+ // Check if this is a request for our fake model
317
+ if (url.includes(fakeModelId)) {
318
+ for (const [filename, content] of Object.entries(fileCache)) {
319
+ if (url.includes(filename)) {
320
+ log(`Serving cached ${filename}`);
321
+ return new Response(content, {
322
+ status: 200,
323
+ headers: { 'Content-Type': 'application/json' },
324
+ });
325
+ }
326
+ }
327
+ // Return 404 for other files (like config.json which tokenizer doesn't need)
328
+ return new Response('Not found', { status: 404 });
329
+ }
330
+
331
+ return originalFetch(input, init);
332
+ };
333
+
334
+ // Disable local model check
335
+ const originalAllowLocal = env.allowLocalModels;
336
+ env.allowLocalModels = false;
337
+
338
+ try {
339
+ const tokenizer = await AutoTokenizer.from_pretrained(fakeModelId);
340
+ log('Tokenizer created successfully');
341
+ return { tokenizer, specialTokens };
342
+ } finally {
343
+ // Restore original state
344
+ globalThis.fetch = originalFetch;
345
+ env.allowLocalModels = originalAllowLocal;
346
+ }
347
+ }
348
+
349
+ export class VLModel {
350
+ constructor() {
351
+ this.tokenizer = null;
352
+ this.embedTokensSession = null;
353
+ this.visionEncoderSession = null;
354
+ this.decoderSession = null;
355
+ this.config = null;
356
+ this.imageTokenId = null;
357
+ this.eosTokenId = null;
358
+ this.hiddenSize = 1024; // Default for 450M
359
+
360
+ // Image embedding cache (persists between turns)
361
+ this.imageCache = new Map(); // URL -> { embeddings, numTokens }
362
+ }
363
+
364
+ /**
365
+ * Clear the image embedding cache (call when starting a new conversation)
366
+ */
367
+ clearImageCache() {
368
+ this.imageCache.clear();
369
+ }
370
+
371
+ /**
372
+ * Load the VL model from a directory
373
+ * @param {string} modelPath - Path to model directory (S3 URL)
374
+ * @param {object} options - Loading options
375
+ * @param {function} options.progressCallback - Progress callback
376
+ * @param {string} options.device - Device to use ('webgpu' or 'wasm')
377
+ * @param {string} options.quantization - Quantization type ('q4', 'q8', or null for fp32)
378
+ */
379
+ async load(modelPath, options = {}) {
380
+ const { progressCallback, device = 'webgpu', quantization = null } = options;
381
+
382
+ const report = (status, progress = 0, file = '') => {
383
+ if (progressCallback) {
384
+ progressCallback({ status, progress, file });
385
+ }
386
+ };
387
+
388
+ // Determine execution provider
389
+ const executionProviders = device === 'webgpu'
390
+ ? ['webgpu', 'wasm']
391
+ : ['wasm'];
392
+
393
+ try {
394
+ // Load tokenizer and extract special token IDs
395
+ report('loading', 0, 'tokenizer');
396
+ const { tokenizer, specialTokens } = await loadTokenizerFromPath(modelPath);
397
+ this.tokenizer = tokenizer;
398
+
399
+ // Load chat template from S3 if not already set in tokenizer
400
+ if (!this.tokenizer.chat_template) {
401
+ try {
402
+ const templateResponse = await fetch(`${modelPath}/chat_template.jinja`, {
403
+ mode: 'cors',
404
+ credentials: 'omit',
405
+ });
406
+ if (templateResponse.ok) {
407
+ const template = await templateResponse.text();
408
+ this.tokenizer.chat_template = template;
409
+ log('Loaded chat template from model path');
410
+ }
411
+ } catch (e) {
412
+ console.warn('Could not load chat template:', e);
413
+ }
414
+ }
415
+
416
+ // Get special token IDs from parsed tokenizer.json
417
+ this.imageTokenId = specialTokens['<image>'] ?? null;
418
+ this.imageStartTokenId = specialTokens['<|image_start|>'] ?? null;
419
+ this.imageEndTokenId = specialTokens['<|image_end|>'] ?? null;
420
+ this.imageSplitTokenId = specialTokens['<|image_split|>'] ?? null;
421
+ this.eosTokenId = this.tokenizer.eos_token_id;
422
+
423
+ log('Image token ID:', this.imageTokenId);
424
+ log('Image start token ID:', this.imageStartTokenId);
425
+ log('Image end token ID:', this.imageEndTokenId);
426
+ log('EOS token ID:', this.eosTokenId);
427
+
428
+ if (this.imageTokenId === null) {
429
+ console.warn('Warning: <image> token not found in tokenizer');
430
+ }
431
+
432
+ // Load config
433
+ report('loading', 10, 'config');
434
+ const configResponse = await fetch(`${modelPath}/config.json`, {
435
+ mode: 'cors',
436
+ credentials: 'omit',
437
+ });
438
+ this.config = await configResponse.json();
439
+ // VL models have config in text_config
440
+ const textConfig = this.config.text_config || this.config;
441
+ this.hiddenSize = textConfig.hidden_size || 1024;
442
+ this.numKVHeads = textConfig.num_key_value_heads || 8;
443
+ this.headDim = Math.floor(this.hiddenSize / (textConfig.num_attention_heads || 16));
444
+ log('Model config:', { hiddenSize: this.hiddenSize, numKVHeads: this.numKVHeads, headDim: this.headDim });
445
+
446
+ // Get external data files (single file per component for 450M)
447
+ const getExternalDataFiles = async (basePath, fileName, fetchOptions) => {
448
+ const files = [];
449
+
450
+ // Get primary file
451
+ const primaryUrl = `${basePath}/onnx/${fileName}.onnx_data`;
452
+ try {
453
+ const headResp = await fetch(primaryUrl, { method: 'HEAD', ...fetchOptions });
454
+ if (!headResp.ok) return []; // No external data
455
+ files.push({
456
+ path: `${fileName}.onnx_data`,
457
+ url: primaryUrl,
458
+ size: parseInt(headResp.headers.get('content-length') || '0', 10)
459
+ });
460
+ } catch (e) {
461
+ return []; // No external data
462
+ }
463
+
464
+ return files;
465
+ };
466
+
467
+ // Helper to load ONNX model with external data (with caching and progress)
468
+ // customProviders allows overriding execution providers for specific sessions
469
+ const loadOnnxWithExternalData = async (name, progress, quantSuffix = quantization, customProviders = null) => {
470
+ // Build filename with optional quantization suffix
471
+ const suffix = quantSuffix ? `_${quantSuffix}` : '';
472
+ const fileName = `${name}${suffix}`;
473
+ report('loading', progress, `${fileName}.onnx`);
474
+
475
+ const onnxPath = `${modelPath}/onnx/${fileName}.onnx`;
476
+ const fetchOptions = { mode: 'cors', credentials: 'omit' };
477
+
478
+ log(`Loading ${fileName}...`);
479
+
480
+ // Progress callback for download progress
481
+ const makeProgressCallback = (file) => (received, total) => {
482
+ const mb = (received / 1024 / 1024).toFixed(0);
483
+ const totalMb = (total / 1024 / 1024).toFixed(0);
484
+ report('loading', progress, `${file}: ${mb} / ${totalMb} MB`);
485
+ };
486
+
487
+ // Get external data files (uses size-based format detection)
488
+ const dataFiles = await getExternalDataFiles(modelPath, fileName, fetchOptions);
489
+ const totalDataSize = dataFiles.reduce((sum, f) => sum + f.size, 0);
490
+ log(`Found ${dataFiles.length} external data file(s) for ${fileName}, total: ${(totalDataSize / 1024 / 1024).toFixed(1)} MB`);
491
+
492
+ // Use custom providers if specified, otherwise use default
493
+ const providers = customProviders || executionProviders;
494
+ const sessionOptions = {
495
+ executionProviders: providers,
496
+ };
497
+
498
+ // Fetch ONNX file (with caching and progress)
499
+ const onnxResponse = await fetchWithCache(onnxPath, fetchOptions, makeProgressCallback(`${fileName}.onnx`));
500
+ if (!onnxResponse.ok) {
501
+ throw new Error(`Failed to fetch ${fileName}.onnx: ${onnxResponse.status}`);
502
+ }
503
+ const onnxBuffer = await onnxResponse.arrayBuffer();
504
+ log(`Loaded ${fileName}.onnx: ${(onnxBuffer.byteLength / 1024 / 1024).toFixed(1)} MB`);
505
+
506
+ if (dataFiles.length > 0) {
507
+ // Load each file individually - use memory for cacheable files, URL for oversized
508
+ sessionOptions.externalData = [];
509
+ for (const f of dataFiles) {
510
+ if (f.size > LARGE_FILE_THRESHOLD) {
511
+ // File too large for JS memory - let ONNX Runtime stream it
512
+ log(`Large file ${f.path} (${(f.size / 1024 / 1024 / 1024).toFixed(2)} GB), using URL-based loading`);
513
+ report('loading', progress, `${fileName} (streaming ${f.path}...)`);
514
+ sessionOptions.externalData.push({
515
+ path: f.path,
516
+ data: f.url,
517
+ });
518
+ } else {
519
+ // File fits in memory - fetch with caching and progress
520
+ const dataResponse = await fetchWithCache(f.url, fetchOptions, makeProgressCallback(f.path));
521
+ if (!dataResponse.ok) {
522
+ throw new Error(`Failed to fetch ${f.path}: ${dataResponse.status}`);
523
+ }
524
+ const dataBuffer = await dataResponse.arrayBuffer();
525
+ log(`Loaded ${f.path}: ${(dataBuffer.byteLength / 1024 / 1024).toFixed(1)} MB`);
526
+ sessionOptions.externalData.push({
527
+ path: f.path,
528
+ data: new Uint8Array(dataBuffer),
529
+ });
530
+ }
531
+ }
532
+ report('loading', progress, `${fileName} (initializing)`);
533
+ } else {
534
+ report('loading', progress, `${fileName} (initializing)`);
535
+ }
536
+
537
+ const session = await ort.InferenceSession.create(new Uint8Array(onnxBuffer), sessionOptions);
538
+ log(`Session created for ${fileName}`);
539
+ return session;
540
+ };
541
+
542
+ // Parse quantization config (can be string for legacy or object for new format)
543
+ const quantConfig = typeof quantization === 'object' ? quantization : {
544
+ decoder: quantization,
545
+ visionEncoder: quantization,
546
+ };
547
+
548
+ // Load embed_tokens (use fp16 suffix if decoder is fp16, otherwise no suffix)
549
+ const embedTokensQuant = quantConfig.decoder || null;
550
+ this.embedTokensSession = await loadOnnxWithExternalData('embed_tokens', 20, embedTokensQuant);
551
+
552
+ // Load vision_encoder (use specified quantization)
553
+ const visionEncoderQuant = quantConfig.visionEncoder || null;
554
+ this.visionEncoderSession = await loadOnnxWithExternalData('vision_encoder', 40, visionEncoderQuant);
555
+
556
+ // Load decoder_model_merged (use specified quantization)
557
+ const decoderQuant = quantConfig.decoder || null;
558
+ this.decoderSession = await loadOnnxWithExternalData('decoder_model_merged', 60, decoderQuant);
559
+
560
+ report('done', 100, '');
561
+ return true;
562
+
563
+ } catch (error) {
564
+ // Better error reporting for ORT errors
565
+ let errorMessage = error;
566
+ if (typeof error === 'number') {
567
+ errorMessage = `ONNX Runtime error code: ${error}. This may indicate a WebGPU memory or compatibility issue.`;
568
+ } else if (error instanceof Error) {
569
+ errorMessage = error.message;
570
+ }
571
+ console.error('Failed to load VL model:', errorMessage);
572
+ throw new Error(errorMessage);
573
+ }
574
+ }
575
+
576
+ /**
577
+ * Process images and get embeddings (with caching)
578
+ * @param {string[]} imageInputs - Array of image URLs or data URLs
579
+ * @returns {Promise<{embeddings: Float32Array, numTokens: number, tokensPerImage: number[]}>}
580
+ */
581
+ async getImageEmbeddings(imageInputs) {
582
+ const allEmbeddings = [];
583
+ const tokensPerImage = [];
584
+ let totalTokens = 0;
585
+ let cacheHits = 0;
586
+ let cacheMisses = 0;
587
+
588
+ for (const input of imageInputs) {
589
+ // Check cache first
590
+ if (this.imageCache.has(input)) {
591
+ const cached = this.imageCache.get(input);
592
+ allEmbeddings.push(cached.embeddings);
593
+ tokensPerImage.push(cached.numTokens);
594
+ totalTokens += cached.numTokens;
595
+ cacheHits++;
596
+ continue;
597
+ }
598
+
599
+ // Cache miss - load and process the image
600
+ cacheMisses++;
601
+ const img = await loadImage(input);
602
+ const processed = await processImage(img);
603
+
604
+ log(`Image processed: ${processed.numTiles} tiles, shape [${processed.shape.join(', ')}]`);
605
+
606
+ // Create tensors - use shape from processed output
607
+ const patchesPerTile = processed.shape[1]; // 1024
608
+
609
+ const pixelValuesTensor = new ort.Tensor(
610
+ 'float32',
611
+ processed.pixelValues,
612
+ processed.shape // [num_tiles, patches_per_tile, 768]
613
+ );
614
+
615
+ const attentionMaskTensor = new ort.Tensor(
616
+ 'int64',
617
+ processed.attentionMask, // BigInt64Array
618
+ [processed.numTiles, patchesPerTile] // [num_tiles, patches_per_tile]
619
+ );
620
+
621
+ const spatialShapesTensor = new ort.Tensor(
622
+ 'int64',
623
+ processed.spatialShapes, // BigInt64Array
624
+ [processed.numTiles, 2] // [num_tiles, 2]
625
+ );
626
+
627
+ // Run vision_encoder
628
+ let outputs = await this.visionEncoderSession.run({
629
+ pixel_values: pixelValuesTensor,
630
+ pixel_attention_mask: attentionMaskTensor,
631
+ spatial_shapes: spatialShapesTensor,
632
+ });
633
+
634
+ // Output shape: [num_image_tokens, hidden_dim] (already flattened)
635
+ let embeddings = outputs.image_features;
636
+ log('Image embeddings shape:', embeddings.dims);
637
+
638
+ // Output is 2D: [num_tokens, hidden_dim]
639
+ const numTokens = embeddings.dims[0];
640
+
641
+ // Store in cache (copy the data since tensor might be reused)
642
+ const embeddingsCopy = new Float32Array(embeddings.data);
643
+ this.imageCache.set(input, { embeddings: embeddingsCopy, numTokens });
644
+
645
+ tokensPerImage.push(numTokens);
646
+ totalTokens += numTokens;
647
+ allEmbeddings.push(embeddingsCopy);
648
+ }
649
+
650
+ if (DEBUG && (cacheHits > 0 || cacheMisses > 1)) {
651
+ log(`Image embeddings: ${cacheHits} cached, ${cacheMisses} computed, ${totalTokens} total tokens`);
652
+ }
653
+
654
+ // Concatenate all image embeddings
655
+ const totalLength = allEmbeddings.reduce((sum, e) => sum + e.length, 0);
656
+ const combined = new Float32Array(totalLength);
657
+ let offset = 0;
658
+ for (const emb of allEmbeddings) {
659
+ combined.set(emb, offset);
660
+ offset += emb.length;
661
+ }
662
+
663
+ return { embeddings: combined, numTokens: totalTokens, tokensPerImage };
664
+ }
665
+
666
+ /**
667
+ * Get text embeddings from token IDs
668
+ * @param {number[]} inputIds - Token IDs as regular numbers
669
+ * @returns {Promise<ort.Tensor>} - Text embeddings tensor
670
+ */
671
+ async getTextEmbeddings(inputIds) {
672
+ const inputTensor = new ort.Tensor(
673
+ 'int64',
674
+ new BigInt64Array(inputIds.map(id => BigInt(id))),
675
+ [1, inputIds.length]
676
+ );
677
+ const outputs = await this.embedTokensSession.run({ input_ids: inputTensor });
678
+ return outputs.inputs_embeds;
679
+ }
680
+
681
+ /**
682
+ * Build combined embeddings by replacing image tokens with image embeddings (1:1)
683
+ * Each <image> token position gets replaced with exactly one image embedding.
684
+ * The sequence length remains the same.
685
+ *
686
+ * @param {number[]} inputIds - Token IDs
687
+ * @param {ort.Tensor} textEmbeddings - Text embeddings tensor
688
+ * @param {Float32Array} imageEmbeddings - Concatenated image embeddings
689
+ */
690
+ buildCombinedEmbeddings1to1(inputIds, textEmbeddings, imageEmbeddings) {
691
+ const [, seqLen, hiddenDim] = textEmbeddings.dims;
692
+ const textEmb = textEmbeddings.data;
693
+ const imgEmb = imageEmbeddings;
694
+
695
+ // Find all image token positions
696
+ const imagePositions = [];
697
+ for (let i = 0; i < inputIds.length; i++) {
698
+ if (inputIds[i] === this.imageTokenId) {
699
+ imagePositions.push(i);
700
+ }
701
+ }
702
+
703
+ const numImageEmbeddings = imgEmb.length / hiddenDim;
704
+ if (imagePositions.length !== numImageEmbeddings) {
705
+ console.warn(`Image token mismatch: ${imagePositions.length} <image> tokens vs ${numImageEmbeddings} embeddings`);
706
+ }
707
+
708
+ // Copy text embeddings and replace image token positions
709
+ const result = new Float32Array(textEmb);
710
+
711
+ for (let i = 0; i < Math.min(imagePositions.length, numImageEmbeddings); i++) {
712
+ const pos = imagePositions[i];
713
+ const embStart = i * hiddenDim;
714
+ const dstStart = pos * hiddenDim;
715
+ result.set(imgEmb.slice(embStart, embStart + hiddenDim), dstStart);
716
+ }
717
+
718
+ return new ort.Tensor('float32', result, [1, seqLen, hiddenDim]);
719
+ }
720
+
721
+ /**
722
+ * Initialize cache for decoder (both conv states and KV cache)
723
+ * Uses float16 tensors as required by the 450M ONNX model
724
+ */
725
+ initializeCache() {
726
+ const cache = {};
727
+
728
+ for (const name of this.decoderSession.inputNames) {
729
+ if (name.startsWith('past_conv')) {
730
+ // Conv states: [batch, hidden_size, kernel_size-1]
731
+ // Kernel size is 4, so we need 3 states
732
+ // Use float16 (Uint16Array) for 450M model compatibility
733
+ cache[name] = new ort.Tensor(
734
+ 'float16',
735
+ new Uint16Array(1 * this.hiddenSize * 3),
736
+ [1, this.hiddenSize, 3]
737
+ );
738
+ } else if (name.startsWith('past_key_values')) {
739
+ // KV cache: [batch, num_kv_heads, past_seq_len, head_dim]
740
+ // Initialize with 0 length sequence
741
+ // Use float16 (Uint16Array) for 450M model compatibility
742
+ cache[name] = new ort.Tensor(
743
+ 'float16',
744
+ new Uint16Array(0), // Empty cache initially
745
+ [1, this.numKVHeads, 0, this.headDim]
746
+ );
747
+ }
748
+ }
749
+
750
+ return cache;
751
+ }
752
+
753
+ /**
754
+ * Update cache from decoder outputs
755
+ */
756
+ updateCache(cache, outputs) {
757
+ for (const name of Object.keys(outputs)) {
758
+ if (name.startsWith('present_conv')) {
759
+ // Conv states: present_conv.X -> past_conv.X
760
+ const cacheName = name.replace('present_conv', 'past_conv');
761
+ if (cacheName in cache) {
762
+ cache[cacheName] = outputs[name];
763
+ }
764
+ } else if (name.startsWith('present.')) {
765
+ // KV cache: present.X.key -> past_key_values.X.key
766
+ const cacheName = name.replace('present.', 'past_key_values.');
767
+ if (cacheName in cache) {
768
+ cache[cacheName] = outputs[name];
769
+ }
770
+ }
771
+ }
772
+ }
773
+
774
+ /**
775
+ * Generate text given messages with optional images
776
+ * @param {Array} messages - Chat messages
777
+ * @param {object} options - Generation options
778
+ */
779
+ async generate(messages, options = {}) {
780
+ const { maxNewTokens = 256, onToken, images = [], messageImageMap = new Map() } = options;
781
+
782
+ log(`=== VL Generate: ${messages.length} messages, ${images.length} images ===`);
783
+
784
+ // Process images FIRST to get patch counts
785
+ let imageEmbeddings = null;
786
+ let tokensPerImage = [];
787
+ let totalImageTokens = 0;
788
+
789
+ if (images.length > 0) {
790
+ const result = await this.getImageEmbeddings(images);
791
+ imageEmbeddings = result.embeddings;
792
+ tokensPerImage = result.tokensPerImage;
793
+ totalImageTokens = result.numTokens;
794
+ log(`Image tokens: ${totalImageTokens} (per-image: [${tokensPerImage.join(', ')}])`);
795
+ }
796
+
797
+ // Build prompt with <image> tokens placed in EACH message that has images
798
+ // This is critical: each user message that sent an image needs its <image> token(s)
799
+ let promptMessages = messages;
800
+ if (images.length > 0) {
801
+ promptMessages = messages.map((msg, idx) => {
802
+ // Check if this message has images via messageImageMap
803
+ if (msg.role === 'user' && messageImageMap.has(idx)) {
804
+ const messageImages = messageImageMap.get(idx);
805
+ const imageTokens = messageImages.map(() => '<image>').join('');
806
+ return { ...msg, content: imageTokens + msg.content };
807
+ }
808
+ return msg;
809
+ });
810
+ }
811
+
812
+ // Apply chat template
813
+ const prompt = this.tokenizer.apply_chat_template(promptMessages, {
814
+ add_generation_prompt: true,
815
+ tokenize: false,
816
+ });
817
+
818
+ // Tokenize
819
+ const encoded = this.tokenizer.encode(prompt);
820
+ let inputIds = [...encoded];
821
+
822
+ // Expand each <image> token to the correct count for that image
823
+ // Add boundary tokens if available: <image_start> [tokens] <image_end>
824
+ if (images.length > 0) {
825
+ const expandedIds = [];
826
+ let imageIdx = 0;
827
+
828
+ for (const id of inputIds) {
829
+ if (id === this.imageTokenId && imageIdx < tokensPerImage.length) {
830
+ // Add start boundary if available
831
+ if (this.imageStartTokenId) {
832
+ expandedIds.push(this.imageStartTokenId);
833
+ }
834
+
835
+ // Replace single <image> with N copies
836
+ const count = tokensPerImage[imageIdx];
837
+ for (let i = 0; i < count; i++) {
838
+ expandedIds.push(this.imageTokenId);
839
+ }
840
+
841
+ // Add end boundary if available
842
+ if (this.imageEndTokenId) {
843
+ expandedIds.push(this.imageEndTokenId);
844
+ }
845
+
846
+ imageIdx++;
847
+ } else {
848
+ expandedIds.push(id);
849
+ }
850
+ }
851
+ inputIds = expandedIds;
852
+ }
853
+
854
+ // Get text embeddings for expanded sequence
855
+ const textEmbeddings = await this.getTextEmbeddings(inputIds);
856
+
857
+ // Replace image token embeddings with actual image embeddings (1:1)
858
+ let inputsEmbeds;
859
+ if (images.length > 0) {
860
+ inputsEmbeds = this.buildCombinedEmbeddings1to1(inputIds, textEmbeddings, imageEmbeddings);
861
+ } else {
862
+ inputsEmbeds = textEmbeddings;
863
+ }
864
+
865
+ log(`Input sequence: ${inputsEmbeds.dims[1]} tokens, ${(inputsEmbeds.data.length * 4 / 1024 / 1024).toFixed(1)} MB`);
866
+
867
+ // Initialize fresh cache for this generation
868
+ // (KV cache is used within generation for autoregressive decoding)
869
+ const cache = this.initializeCache();
870
+
871
+ // Generation loop
872
+ const seqLen = inputsEmbeds.dims[1];
873
+ let curLen = seqLen;
874
+ let currentEmbeds = inputsEmbeds;
875
+ const generatedTokens = [];
876
+
877
+ for (let step = 0; step < maxNewTokens; step++) {
878
+ // Prepare attention mask
879
+ const attentionMask = new ort.Tensor(
880
+ 'int64',
881
+ new BigInt64Array(curLen).fill(1n),
882
+ [1, curLen]
883
+ );
884
+
885
+ // Run decoder (LFM2 models don't use position_ids - position is implicit from attention)
886
+ const feeds = {
887
+ inputs_embeds: currentEmbeds,
888
+ attention_mask: attentionMask,
889
+ ...cache,
890
+ };
891
+
892
+ const outputs = await this.decoderSession.run(feeds);
893
+
894
+ // Get logits - shape is [batch, seq_len, vocab_size]
895
+ const logits = outputs.logits;
896
+ const vocabSize = logits.dims[2];
897
+ const logitsData = logits.data;
898
+
899
+ // Get last token logits
900
+ const lastLogitStart = (logits.dims[1] - 1) * vocabSize;
901
+ const lastLogits = logitsData.slice(lastLogitStart, lastLogitStart + vocabSize);
902
+
903
+ // Greedy decoding - find max
904
+ let maxIdx = 0;
905
+ let maxVal = lastLogits[0];
906
+ for (let i = 1; i < vocabSize; i++) {
907
+ if (lastLogits[i] > maxVal) {
908
+ maxVal = lastLogits[i];
909
+ maxIdx = i;
910
+ }
911
+ }
912
+
913
+ generatedTokens.push(maxIdx);
914
+
915
+ // Callback with token
916
+ if (onToken) {
917
+ const tokenText = this.tokenizer.decode([maxIdx]);
918
+ const shouldStop = onToken(tokenText, maxIdx);
919
+ if (shouldStop) break;
920
+ }
921
+
922
+ // Check for EOS
923
+ if (maxIdx === this.eosTokenId) {
924
+ break;
925
+ }
926
+
927
+ // Update cache for next token
928
+ this.updateCache(cache, outputs);
929
+
930
+ // Get embedding for next token
931
+ const nextEmbeds = await this.getTextEmbeddings([maxIdx]);
932
+ currentEmbeds = nextEmbeds;
933
+ curLen++;
934
+ }
935
+
936
+ return this.tokenizer.decode(generatedTokens, { skip_special_tokens: true });
937
+ }
938
+
939
+ /**
940
+ * Free resources
941
+ */
942
+ async dispose() {
943
+ this.clearImageCache();
944
+ this.tokenizer = null;
945
+
946
+ // Properly release ONNX sessions to free GPU resources
947
+ if (this.embedTokensSession) {
948
+ try {
949
+ await this.embedTokensSession.release();
950
+ } catch (e) {
951
+ console.warn('Error releasing embedTokensSession:', e);
952
+ }
953
+ this.embedTokensSession = null;
954
+ }
955
+ if (this.visionEncoderSession) {
956
+ try {
957
+ await this.visionEncoderSession.release();
958
+ } catch (e) {
959
+ console.warn('Error releasing visionEncoderSession:', e);
960
+ }
961
+ this.visionEncoderSession = null;
962
+ }
963
+ if (this.decoderSession) {
964
+ try {
965
+ await this.decoderSession.release();
966
+ } catch (e) {
967
+ console.warn('Error releasing decoderSession:', e);
968
+ }
969
+ this.decoderSession = null;
970
+ }
971
+ }
972
+ }
973
+
974
+ export default VLModel;
vl-processor.js ADDED
@@ -0,0 +1,497 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * LFM2-VL Image Processor for WebGPU/ONNX Runtime Web
3
+ *
4
+ * Implements the image preprocessing logic from Lfm2VlImageProcessorFast:
5
+ * 1. Split image into tiles (512x512)
6
+ * 2. Extract 16x16 patches from each tile (32x32 = 1024 patches per tile)
7
+ * 3. Flatten each patch to 768 values (16*16*3)
8
+ * 4. Normalize: (pixel / 255 - 0.5) / 0.5 = pixel / 127.5 - 1
9
+ *
10
+ * Output shapes match Python processor:
11
+ * - pixel_values: [num_tiles, 1024, 768]
12
+ * - pixel_attention_mask: [num_tiles, 1024]
13
+ */
14
+
15
+ // Configuration from preprocessor_config.json
16
+ const CONFIG = {
17
+ tileSize: 512,
18
+ maxTiles: 10,
19
+ minTiles: 2,
20
+ imageMean: [0.5, 0.5, 0.5],
21
+ imageStd: [0.5, 0.5, 0.5],
22
+ rescaleFactor: 1 / 255,
23
+ useThumbnail: false, // LFM2-VL-450M does not use thumbnail
24
+ patchSize: 16, // Each patch is 16x16 pixels
25
+ patchesPerTile: 32, // 512 / 16 = 32 patches per side = 1024 per tile
26
+ downsampleFactor: 2,
27
+ minImageTokens: 64,
28
+ maxImageTokens: 256,
29
+ maxPixelsTolerance: 2.0,
30
+ };
31
+
32
+ // Pre-computed normalization constants for faster patch extraction
33
+ // Formula: (pixel / 255 - 0.5) / 0.5 = pixel / 127.5 - 1.0
34
+ const NORM_SCALE = 1 / 127.5;
35
+ const NORM_OFFSET = -1.0;
36
+
37
+ // Pre-computed patch info for common live-caption resolutions (all 32-aligned)
38
+ const PRECOMPUTED_SIZES = {
39
+ 256: { width: 256, height: 256, patchesH: 16, patchesW: 16 }, // 256/16 = 16
40
+ 384: { width: 384, height: 384, patchesH: 24, patchesW: 24 }, // 384/16 = 24
41
+ 448: { width: 448, height: 448, patchesH: 28, patchesW: 28 }, // 448/16 = 28
42
+ 512: { width: 512, height: 512, patchesH: 32, patchesW: 32 }, // 512/16 = 32
43
+ };
44
+
45
+ /**
46
+ * Round number to closest value divisible by factor
47
+ */
48
+ function roundByFactor(number, factor) {
49
+ return Math.round(number / factor) * factor;
50
+ }
51
+
52
+ /**
53
+ * Ceil number to smallest value >= number divisible by factor
54
+ */
55
+ function ceilByFactor(number, factor) {
56
+ return Math.ceil(number / factor) * factor;
57
+ }
58
+
59
+ /**
60
+ * Floor number to largest value <= number divisible by factor
61
+ */
62
+ function floorByFactor(number, factor) {
63
+ return Math.floor(number / factor) * factor;
64
+ }
65
+
66
+ /**
67
+ * Find the closest aspect ratio from target ratios to match input aspect ratio
68
+ * Matches Python's find_closest_aspect_ratio()
69
+ */
70
+ function findClosestAspectRatio(aspectRatio, targetRatios, width, height, imageSize) {
71
+ let bestRatioDiff = Infinity;
72
+ let bestRatio = [1, 1];
73
+ const area = width * height;
74
+
75
+ for (const ratio of targetRatios) {
76
+ const targetAspectRatio = ratio[0] / ratio[1];
77
+ const ratioDiff = Math.abs(aspectRatio - targetAspectRatio);
78
+
79
+ if (ratioDiff < bestRatioDiff) {
80
+ bestRatioDiff = ratioDiff;
81
+ bestRatio = ratio;
82
+ } else if (ratioDiff === bestRatioDiff) {
83
+ // If equally close, prefer ratio that better matches original image area
84
+ const targetArea = imageSize * imageSize * ratio[0] * ratio[1];
85
+ if (area > 0.5 * targetArea) {
86
+ bestRatio = ratio;
87
+ }
88
+ }
89
+ }
90
+
91
+ return bestRatio;
92
+ }
93
+
94
+ /**
95
+ * Check if image is too large to process as one tile
96
+ * Matches Python's _is_img_too_large()
97
+ */
98
+ function isImageTooLarge(width, height) {
99
+ const { patchSize, maxImageTokens, downsampleFactor, maxPixelsTolerance } = CONFIG;
100
+ const hBar = Math.max(patchSize, roundByFactor(height, patchSize));
101
+ const wBar = Math.max(patchSize, roundByFactor(width, patchSize));
102
+ const maxPixels = maxImageTokens * (patchSize ** 2) * (downsampleFactor ** 2) * maxPixelsTolerance;
103
+ return hBar * wBar > maxPixels;
104
+ }
105
+
106
+ /**
107
+ * Smart resize to ensure dimensions divisible by patchSize * downsampleFactor
108
+ * and total pixels within [minPixels, maxPixels]
109
+ * Matches Python's _smart_resize()
110
+ * @returns {{width: number, height: number}}
111
+ */
112
+ function smartResize(width, height) {
113
+ const { patchSize, downsampleFactor, minImageTokens, maxImageTokens } = CONFIG;
114
+ const totalFactor = patchSize * downsampleFactor; // 32
115
+ const minPixels = minImageTokens * (patchSize ** 2) * (downsampleFactor ** 2);
116
+ const maxPixels = maxImageTokens * (patchSize ** 2) * (downsampleFactor ** 2);
117
+
118
+ let hBar = Math.max(totalFactor, roundByFactor(height, totalFactor));
119
+ let wBar = Math.max(totalFactor, roundByFactor(width, totalFactor));
120
+
121
+ if (hBar * wBar > maxPixels) {
122
+ const beta = Math.sqrt((height * width) / maxPixels);
123
+ hBar = Math.max(totalFactor, floorByFactor(height / beta, totalFactor));
124
+ wBar = Math.max(totalFactor, floorByFactor(width / beta, totalFactor));
125
+ } else if (hBar * wBar < minPixels) {
126
+ const beta = Math.sqrt(minPixels / (height * width));
127
+ hBar = ceilByFactor(height * beta, totalFactor);
128
+ wBar = ceilByFactor(width * beta, totalFactor);
129
+ }
130
+
131
+ return { width: wBar, height: hBar };
132
+ }
133
+
134
+ /**
135
+ * Get number of tokens for an image of given dimensions
136
+ * Matches Python's _get_tokens_num()
137
+ */
138
+ function getTokensNum(height, width) {
139
+ const { patchSize, downsampleFactor } = CONFIG;
140
+ const numPatchesHeight = Math.floor(height / patchSize);
141
+ const numPatchesWidth = Math.floor(width / patchSize);
142
+ const dwnNumPatchesHeight = Math.ceil(numPatchesHeight / downsampleFactor);
143
+ const dwnNumPatchesWidth = Math.ceil(numPatchesWidth / downsampleFactor);
144
+ return dwnNumPatchesHeight * dwnNumPatchesWidth;
145
+ }
146
+
147
+ /**
148
+ * Calculate optimal tile grid for an image
149
+ * Matches Python's _high_res_preprocessor() grid selection
150
+ * @param {number} width - Image width
151
+ * @param {number} height - Image height
152
+ * @returns {{rows: number, cols: number}} - Tile grid dimensions
153
+ */
154
+ function calculateTileGrid(width, height) {
155
+ const { tileSize, minTiles, maxTiles } = CONFIG;
156
+ const aspectRatio = width / height;
157
+
158
+ // Generate valid patch grid configurations (width, height)
159
+ // Matches Python: [(w, h) for n in range(min_tiles, max_tiles + 1) for w in range(1, n + 1) for h in range(1, n + 1) if min_tiles <= w * h <= max_tiles]
160
+ const targetRatios = [];
161
+ for (let n = minTiles; n <= maxTiles; n++) {
162
+ for (let w = 1; w <= n; w++) {
163
+ for (let h = 1; h <= n; h++) {
164
+ if (w * h >= minTiles && w * h <= maxTiles) {
165
+ // Check if already exists
166
+ if (!targetRatios.some(r => r[0] === w && r[1] === h)) {
167
+ targetRatios.push([w, h]);
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }
173
+ // Sort by total tiles
174
+ targetRatios.sort((a, b) => (a[0] * a[1]) - (b[0] * b[1]));
175
+
176
+ if (targetRatios.length === 0) {
177
+ return { rows: 1, cols: 1 };
178
+ }
179
+
180
+ // Find best matching grid configuration
181
+ const [gridWidth, gridHeight] = findClosestAspectRatio(
182
+ aspectRatio, targetRatios, width, height, tileSize
183
+ );
184
+
185
+ return { rows: gridHeight, cols: gridWidth };
186
+ }
187
+
188
+ /**
189
+ * Process an image into flattened patches for VL model
190
+ * Matches Python's _resize_and_maybe_split() logic
191
+ * @param {HTMLImageElement|HTMLCanvasElement|ImageData} image - Input image or raw ImageData
192
+ * @returns {Promise<{pixelValues: Float32Array, attentionMask: BigInt64Array, numTiles: number, shape: number[]}>}
193
+ */
194
+ export async function processImage(image) {
195
+ let width, height;
196
+ let inputImageData = null; // For direct ImageData input
197
+
198
+ if (image instanceof ImageData) {
199
+ // Direct ImageData input - skip canvas creation entirely
200
+ width = image.width;
201
+ height = image.height;
202
+ inputImageData = image;
203
+ } else if (image instanceof HTMLImageElement) {
204
+ width = image.naturalWidth;
205
+ height = image.naturalHeight;
206
+ } else {
207
+ width = image.width;
208
+ height = image.height;
209
+ }
210
+
211
+ const { tileSize, patchSize, useThumbnail } = CONFIG;
212
+ const patchesPerSide = CONFIG.patchesPerTile; // 32
213
+ const maxPatchesPerTile = patchesPerSide * patchesPerSide; // 1024
214
+ const patchDim = patchSize * patchSize * 3; // 768
215
+
216
+ // Check if image needs splitting (matches Python's _resize_and_maybe_split)
217
+ const needsSplitting = isImageTooLarge(width, height);
218
+
219
+ if (needsSplitting) {
220
+ // HIGH-RES PATH: Split into tiles + optional thumbnail
221
+ // Matches Python's _high_res_preprocessor()
222
+ const { rows, cols } = calculateTileGrid(width, height);
223
+ const totalGridTiles = rows * cols;
224
+
225
+ // Only use tiling if we get more than 1 tile
226
+ if (totalGridTiles > 1) {
227
+ const numTiles = totalGridTiles + (useThumbnail ? 1 : 0);
228
+
229
+ // Output arrays - use max patches per tile for uniform shape
230
+ const pixelValues = new Float32Array(numTiles * maxPatchesPerTile * patchDim);
231
+ const attentionMask = new BigInt64Array(numTiles * maxPatchesPerTile);
232
+ const spatialShapes = new BigInt64Array(numTiles * 2);
233
+
234
+ // STEP 1: Resize ENTIRE image to target grid dimensions (matches Python)
235
+ const targetWidth = tileSize * cols;
236
+ const targetHeight = tileSize * rows;
237
+
238
+ const resizedCanvas = document.createElement('canvas');
239
+ resizedCanvas.width = targetWidth;
240
+ resizedCanvas.height = targetHeight;
241
+ const resizedCtx = resizedCanvas.getContext('2d');
242
+ resizedCtx.drawImage(image, 0, 0, targetWidth, targetHeight);
243
+
244
+ // STEP 2: Extract tiles by CROPPING from resized image (matches Python)
245
+ let tileIdx = 0;
246
+ for (let row = 0; row < rows; row++) {
247
+ for (let col = 0; col < cols; col++) {
248
+ const tileCanvas = document.createElement('canvas');
249
+ tileCanvas.width = tileSize;
250
+ tileCanvas.height = tileSize;
251
+ const tileCtx = tileCanvas.getContext('2d');
252
+
253
+ // Crop tile from resized image
254
+ tileCtx.drawImage(
255
+ resizedCanvas,
256
+ col * tileSize, row * tileSize, tileSize, tileSize, // source crop
257
+ 0, 0, tileSize, tileSize // dest (same size, no scaling)
258
+ );
259
+
260
+ const tileData = tileCtx.getImageData(0, 0, tileSize, tileSize);
261
+ extractPatchesFromFullTile(tileData, pixelValues, attentionMask, tileIdx, patchesPerSide, maxPatchesPerTile);
262
+
263
+ // Spatial shape for this tile
264
+ spatialShapes[tileIdx * 2] = BigInt(patchesPerSide); // height in patches
265
+ spatialShapes[tileIdx * 2 + 1] = BigInt(patchesPerSide); // width in patches
266
+
267
+ tileIdx++;
268
+ }
269
+ }
270
+
271
+ // STEP 3: Add thumbnail LAST (matches Python - thumbnail is appended)
272
+ // Thumbnail uses smart resize to variable dimensions (like single-tile path)
273
+ if (useThumbnail) {
274
+ const thumbResized = smartResize(width, height);
275
+ const thumbWidth = thumbResized.width;
276
+ const thumbHeight = thumbResized.height;
277
+
278
+ const thumbCanvas = document.createElement('canvas');
279
+ thumbCanvas.width = thumbWidth;
280
+ thumbCanvas.height = thumbHeight;
281
+ const thumbCtx = thumbCanvas.getContext('2d');
282
+ thumbCtx.drawImage(image, 0, 0, thumbWidth, thumbHeight);
283
+
284
+ const thumbData = thumbCtx.getImageData(0, 0, thumbWidth, thumbHeight);
285
+ const thumbPatchesH = thumbHeight / patchSize;
286
+ const thumbPatchesW = thumbWidth / patchSize;
287
+
288
+ extractPatchesFromVariableSize(thumbData, pixelValues, attentionMask, tileIdx, thumbPatchesH, thumbPatchesW, maxPatchesPerTile);
289
+
290
+ // Spatial shape for thumbnail (variable based on smart resize)
291
+ spatialShapes[tileIdx * 2] = BigInt(thumbPatchesH);
292
+ spatialShapes[tileIdx * 2 + 1] = BigInt(thumbPatchesW);
293
+
294
+ tileIdx++;
295
+ }
296
+
297
+ return {
298
+ pixelValues,
299
+ attentionMask,
300
+ spatialShapes,
301
+ numTiles,
302
+ shape: [numTiles, maxPatchesPerTile, patchDim],
303
+ };
304
+ }
305
+ }
306
+
307
+ // SINGLE-TILE PATH: Smart resize only (no splitting)
308
+ // Matches Python's else branch in _resize_and_maybe_split()
309
+
310
+ let resizedWidth, resizedHeight, actualPatchesH, actualPatchesW;
311
+ let imageData;
312
+
313
+ // OPTIMIZATION: Check if dimensions are pre-computed (32-aligned live caption sizes)
314
+ const precomputed = PRECOMPUTED_SIZES[width];
315
+ const isAlreadyAligned = precomputed && width === height;
316
+
317
+ if (inputImageData && isAlreadyAligned) {
318
+ // FAST PATH: Direct ImageData with known dimensions - skip all resizing
319
+ resizedWidth = width;
320
+ resizedHeight = height;
321
+ actualPatchesH = precomputed.patchesH;
322
+ actualPatchesW = precomputed.patchesW;
323
+ imageData = inputImageData;
324
+ } else if (isAlreadyAligned) {
325
+ // Dimensions already 32-aligned, skip smartResize computation
326
+ resizedWidth = width;
327
+ resizedHeight = height;
328
+ actualPatchesH = precomputed.patchesH;
329
+ actualPatchesW = precomputed.patchesW;
330
+
331
+ // Still need to get ImageData from the image
332
+ const resizedCanvas = document.createElement('canvas');
333
+ resizedCanvas.width = resizedWidth;
334
+ resizedCanvas.height = resizedHeight;
335
+ const resizedCtx = resizedCanvas.getContext('2d');
336
+ resizedCtx.drawImage(image, 0, 0, resizedWidth, resizedHeight);
337
+ imageData = resizedCtx.getImageData(0, 0, resizedWidth, resizedHeight);
338
+ } else {
339
+ // Standard path: compute smart resize
340
+ const resized = smartResize(width, height);
341
+ resizedWidth = resized.width;
342
+ resizedHeight = resized.height;
343
+ actualPatchesH = resizedHeight / patchSize;
344
+ actualPatchesW = resizedWidth / patchSize;
345
+
346
+ // Create canvas at actual resized dimensions
347
+ const resizedCanvas = document.createElement('canvas');
348
+ resizedCanvas.width = resizedWidth;
349
+ resizedCanvas.height = resizedHeight;
350
+ const resizedCtx = resizedCanvas.getContext('2d');
351
+ resizedCtx.drawImage(image, 0, 0, resizedWidth, resizedHeight);
352
+ imageData = resizedCtx.getImageData(0, 0, resizedWidth, resizedHeight);
353
+ }
354
+
355
+ const numTiles = 1;
356
+ const pixelValues = new Float32Array(numTiles * maxPatchesPerTile * patchDim);
357
+ const attentionMask = new BigInt64Array(numTiles * maxPatchesPerTile);
358
+ const spatialShapes = new BigInt64Array(numTiles * 2);
359
+
360
+ extractPatchesFromVariableSize(imageData, pixelValues, attentionMask, 0, actualPatchesH, actualPatchesW, maxPatchesPerTile);
361
+
362
+ spatialShapes[0] = BigInt(actualPatchesH);
363
+ spatialShapes[1] = BigInt(actualPatchesW);
364
+
365
+ return {
366
+ pixelValues,
367
+ attentionMask,
368
+ spatialShapes,
369
+ numTiles,
370
+ shape: [numTiles, maxPatchesPerTile, patchDim],
371
+ };
372
+ }
373
+
374
+ /**
375
+ * Extract patches from a full 512x512 tile (all patches are valid)
376
+ * @param {ImageData} tileData - Tile image data (512x512)
377
+ * @param {Float32Array} pixelValues - Output pixel values array
378
+ * @param {BigInt64Array} attentionMask - Output attention mask array
379
+ * @param {number} tileIdx - Index of this tile
380
+ * @param {number} patchesPerSide - Number of patches per side (32 for 512x512)
381
+ * @param {number} maxPatchesPerTile - Max patches per tile for array indexing (1024)
382
+ */
383
+ function extractPatchesFromFullTile(tileData, pixelValues, attentionMask, tileIdx, patchesPerSide, maxPatchesPerTile) {
384
+ const patchSize = CONFIG.patchSize;
385
+ const patchDim = patchSize * patchSize * 3;
386
+ const tileWidth = tileData.width;
387
+
388
+ const pixels = tileData.data;
389
+ const tileOffset = tileIdx * maxPatchesPerTile * patchDim;
390
+ const maskOffset = tileIdx * maxPatchesPerTile;
391
+
392
+ let patchIdx = 0;
393
+
394
+ for (let py = 0; py < patchesPerSide; py++) {
395
+ for (let px = 0; px < patchesPerSide; px++) {
396
+ const patchStartX = px * patchSize;
397
+ const patchStartY = py * patchSize;
398
+
399
+ // All patches in full tile are valid
400
+ attentionMask[maskOffset + patchIdx] = 1n;
401
+
402
+ // Extract and normalize patch pixels using pre-computed constants
403
+ const patchOffset = tileOffset + patchIdx * patchDim;
404
+ let outIdx = 0;
405
+
406
+ // Flatten patch: iterate over pixels in patch, then channels
407
+ // Optimized: srcIdx calculated once per pixel, use pre-computed normalization
408
+ for (let dy = 0; dy < patchSize; dy++) {
409
+ const rowOffset = (patchStartY + dy) * tileWidth;
410
+ for (let dx = 0; dx < patchSize; dx++) {
411
+ const srcIdx = (rowOffset + patchStartX + dx) * 4;
412
+ // Optimized normalization: pixel * (1/127.5) - 1.0
413
+ pixelValues[patchOffset + outIdx++] = pixels[srcIdx] * NORM_SCALE + NORM_OFFSET;
414
+ pixelValues[patchOffset + outIdx++] = pixels[srcIdx + 1] * NORM_SCALE + NORM_OFFSET;
415
+ pixelValues[patchOffset + outIdx++] = pixels[srcIdx + 2] * NORM_SCALE + NORM_OFFSET;
416
+ }
417
+ }
418
+
419
+ patchIdx++;
420
+ }
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Extract patches from variable-sized image and pad to maxPatchesPerTile
426
+ * Matches Python's convert_image_to_patches + pad_along_first_dim
427
+ * @param {ImageData} imageData - Image data at actual dimensions
428
+ * @param {Float32Array} pixelValues - Output pixel values array
429
+ * @param {BigInt64Array} attentionMask - Output attention mask array
430
+ * @param {number} tileIdx - Index of this tile
431
+ * @param {number} patchesH - Number of patches in height
432
+ * @param {number} patchesW - Number of patches in width
433
+ * @param {number} maxPatchesPerTile - Max patches per tile for padding (1024)
434
+ */
435
+ function extractPatchesFromVariableSize(imageData, pixelValues, attentionMask, tileIdx, patchesH, patchesW, maxPatchesPerTile) {
436
+ const patchSize = CONFIG.patchSize;
437
+ const patchDim = patchSize * patchSize * 3;
438
+ const imageWidth = imageData.width;
439
+
440
+ const pixels = imageData.data;
441
+ const tileOffset = tileIdx * maxPatchesPerTile * patchDim;
442
+ const maskOffset = tileIdx * maxPatchesPerTile;
443
+
444
+ const actualPatches = patchesH * patchesW;
445
+
446
+ // Extract actual patches
447
+ let patchIdx = 0;
448
+ for (let py = 0; py < patchesH; py++) {
449
+ for (let px = 0; px < patchesW; px++) {
450
+ const patchStartX = px * patchSize;
451
+ const patchStartY = py * patchSize;
452
+
453
+ // Mark as valid
454
+ attentionMask[maskOffset + patchIdx] = 1n;
455
+
456
+ // Extract and normalize patch pixels using pre-computed constants
457
+ const patchOffset = tileOffset + patchIdx * patchDim;
458
+ let outIdx = 0;
459
+
460
+ // Flatten patch: iterate over pixels in patch, then channels
461
+ // Optimized: srcIdx calculated once per pixel, use pre-computed normalization
462
+ for (let dy = 0; dy < patchSize; dy++) {
463
+ const rowOffset = (patchStartY + dy) * imageWidth;
464
+ for (let dx = 0; dx < patchSize; dx++) {
465
+ const srcIdx = (rowOffset + patchStartX + dx) * 4;
466
+ // Optimized normalization: pixel * (1/127.5) - 1.0
467
+ pixelValues[patchOffset + outIdx++] = pixels[srcIdx] * NORM_SCALE + NORM_OFFSET;
468
+ pixelValues[patchOffset + outIdx++] = pixels[srcIdx + 1] * NORM_SCALE + NORM_OFFSET;
469
+ pixelValues[patchOffset + outIdx++] = pixels[srcIdx + 2] * NORM_SCALE + NORM_OFFSET;
470
+ }
471
+ }
472
+
473
+ patchIdx++;
474
+ }
475
+ }
476
+
477
+ // Pad remaining patches with zeros and mask = 0
478
+ for (let i = actualPatches; i < maxPatchesPerTile; i++) {
479
+ attentionMask[maskOffset + i] = 0n;
480
+ // pixelValues already initialized to 0
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Load an image from URL or data URL
486
+ * @param {string} src - Image URL or data URL
487
+ * @returns {Promise<HTMLImageElement>}
488
+ */
489
+ export function loadImage(src) {
490
+ return new Promise((resolve, reject) => {
491
+ const img = new Image();
492
+ img.crossOrigin = 'anonymous';
493
+ img.onload = () => resolve(img);
494
+ img.onerror = reject;
495
+ img.src = src;
496
+ });
497
+ }
webgpu-inference.js ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * WebGPU Inference Wrapper
3
+ * Provides a clean interface between the app and the VL model
4
+ */
5
+
6
+ import { VLModel, clearModelCache, getCacheInfo, setDebug } from './vl-model.js';
7
+ import { getModelConfig } from './config.js';
8
+
9
+ // Expose debug toggle on window for browser console access
10
+ window.setDebug = setDebug;
11
+
12
+ // Re-export cache utilities
13
+ export { clearModelCache, getCacheInfo, setDebug };
14
+
15
+ export class WebGPUInference {
16
+ constructor() {
17
+ this.model = null;
18
+ this.currentModelId = null;
19
+ this.isLoading = false;
20
+ this.isReady = false;
21
+ }
22
+
23
+ /**
24
+ * Load a model
25
+ * @param {string} modelId - Model ID from config
26
+ * @param {object} options - Loading options
27
+ * @param {function} options.progressCallback - Progress callback
28
+ */
29
+ async loadModel(modelId, options = {}) {
30
+ if (this.isLoading) {
31
+ throw new Error('Model is already loading');
32
+ }
33
+
34
+ if (this.currentModelId === modelId && this.isReady) {
35
+ return;
36
+ }
37
+
38
+ this.isLoading = true;
39
+ this.isReady = false;
40
+
41
+ try {
42
+ const modelConfig = getModelConfig(modelId);
43
+ if (!modelConfig) {
44
+ throw new Error(`Model configuration not found: ${modelId}`);
45
+ }
46
+
47
+ // Dispose old model if exists
48
+ if (this.model) {
49
+ this.model.dispose();
50
+ this.model = null;
51
+ }
52
+
53
+ // Create new model instance
54
+ this.model = new VLModel();
55
+
56
+ // Load the model with quantization from config
57
+ await this.model.load(modelConfig.path, {
58
+ device: 'webgpu',
59
+ quantization: modelConfig.quantization || { decoder: null, visionEncoder: null },
60
+ progressCallback: options.progressCallback,
61
+ });
62
+
63
+ this.currentModelId = modelId;
64
+ this.isReady = true;
65
+
66
+ } catch (error) {
67
+ this.model = null;
68
+ this.currentModelId = null;
69
+ this.isReady = false;
70
+ throw error;
71
+ } finally {
72
+ this.isLoading = false;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Generate a response from messages
78
+ * @param {Array<Object>} messages - Array of message objects with role and content
79
+ * @param {object} options - Generation options
80
+ * @param {function} options.onToken - Token callback for streaming
81
+ * @returns {Promise<string>} Generated response
82
+ */
83
+ async generate(messages, options = {}) {
84
+ if (!this.isReady || !this.model) {
85
+ throw new Error('Model not loaded. Please load a model first.');
86
+ }
87
+
88
+ // Convert app message format to VL model format
89
+ const { vlMessages, images, messageImageMap } = this.convertMessages(messages);
90
+
91
+ // Generate response
92
+ return await this.model.generate(vlMessages, {
93
+ maxNewTokens: options.maxNewTokens || 512,
94
+ images: images,
95
+ messageImageMap: messageImageMap,
96
+ onToken: options.onToken,
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Convert app message format to VL model format
102
+ * @param {Array<Object>} messages - App messages
103
+ * @returns {{vlMessages: Array, images: Array<string>, messageImageMap: Map}}
104
+ */
105
+ convertMessages(messages) {
106
+ const vlMessages = [];
107
+ const images = [];
108
+ const messageImageMap = new Map();
109
+
110
+ for (const message of messages) {
111
+ const { role, content } = message;
112
+
113
+ if (typeof content === 'string') {
114
+ vlMessages.push({ role, content });
115
+ } else if (Array.isArray(content)) {
116
+ let textContent = '';
117
+ const messageImages = [];
118
+
119
+ for (const item of content) {
120
+ if (item.type === 'text') {
121
+ textContent += item.value;
122
+ } else if (item.type === 'image') {
123
+ messageImages.push(item.value);
124
+ images.push(item.value);
125
+ }
126
+ }
127
+
128
+ if (textContent.trim() || messageImages.length > 0) {
129
+ if (messageImages.length > 0) {
130
+ messageImageMap.set(vlMessages.length, messageImages);
131
+ }
132
+ vlMessages.push({ role, content: textContent || '' });
133
+ }
134
+ } else {
135
+ vlMessages.push({ role, content: String(content || '') });
136
+ }
137
+ }
138
+
139
+ return { vlMessages, images, messageImageMap };
140
+ }
141
+
142
+ /**
143
+ * Check if a model is loaded
144
+ * @returns {boolean}
145
+ */
146
+ isModelLoaded() {
147
+ return this.isReady;
148
+ }
149
+
150
+ /**
151
+ * Get current model ID
152
+ * @returns {string|null}
153
+ */
154
+ getCurrentModelId() {
155
+ return this.currentModelId;
156
+ }
157
+
158
+ /**
159
+ * Clear the image embedding cache
160
+ */
161
+ clearImageCache() {
162
+ if (this.model) {
163
+ this.model.clearImageCache();
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Dispose the model and free resources
169
+ */
170
+ dispose() {
171
+ if (this.model) {
172
+ this.model.dispose();
173
+ this.model = null;
174
+ }
175
+ this.currentModelId = null;
176
+ this.isReady = false;
177
+ }
178
+ }
179
+
180
+ // Singleton instance
181
+ let webgpuInstance = null;
182
+
183
+ /**
184
+ * Get the WebGPU inference singleton
185
+ * @returns {WebGPUInference}
186
+ */
187
+ export function getWebGPUInference() {
188
+ if (!webgpuInstance) {
189
+ webgpuInstance = new WebGPUInference();
190
+ }
191
+ return webgpuInstance;
192
+ }