emilstabil commited on
Commit
1c7293d
·
verified ·
1 Parent(s): 3f88ea4

Cache verified model files across browser visits

Browse files
Files changed (5) hide show
  1. README.md +7 -4
  2. app.js +5 -5
  3. index.html +1 -1
  4. pnc-worker.js +44 -32
  5. worker.js +44 -33
README.md CHANGED
@@ -23,10 +23,12 @@ saved.
23
 
24
  The model URLs and checksums are pinned in [`model-config.json`](model-config.json).
25
  The browser downloads Tiny's runtime configuration and ONNX files, then PnC's
26
- files, directly from their Hugging Face model repos. The small third-party
27
- Silero VAD file is bundled with the Space for browser access. Microphone access
28
- requires a secure origin and the browser's permission prompt, shown after the
29
- app's introductory mic dialog. Browser support remains a preview while
 
 
30
  physical-device and mobile checks are completed.
31
 
32
  The browser and Python CLI pin the same PnC v2 int8 artifact. The Python
@@ -78,4 +80,5 @@ node tests/browser_live_chunking.mjs
78
  node tests/browser_pnc_sentence_casing.mjs
79
  node tests/browser_space_signed_assets.mjs
80
  node tests/browser_tiny_runtime_config.mjs
 
81
  ```
 
23
 
24
  The model URLs and checksums are pinned in [`model-config.json`](model-config.json).
25
  The browser downloads Tiny's runtime configuration and ONNX files, then PnC's
26
+ files, directly from their Hugging Face model repos. Verified model files are
27
+ kept in browser storage for later visits when storage is available; every load
28
+ still verifies their checksums and initializes the WebAssembly runtime. The
29
+ small third-party Silero VAD file is bundled with the Space for browser access.
30
+ Microphone access requires a secure origin and the browser's permission prompt,
31
+ shown after the app's introductory mic dialog. Browser support remains a preview while
32
  physical-device and mobile checks are completed.
33
 
34
  The browser and Python CLI pin the same PnC v2 int8 artifact. The Python
 
80
  node tests/browser_pnc_sentence_casing.mjs
81
  node tests/browser_space_signed_assets.mjs
82
  node tests/browser_tiny_runtime_config.mjs
83
+ node tests/browser_model_cache.mjs
84
  ```
app.js CHANGED
@@ -91,8 +91,8 @@ const state = {
91
  level: 0,
92
  };
93
 
94
- const worker = new Worker(signedAsset("worker.js?v=20260927-1"));
95
- const pncWorker = new Worker(signedAsset("pnc-worker.js?v=20260927-1"));
96
 
97
  function setStatus(text, detail = "", kind = "loading") {
98
  elements.statusText.textContent = text;
@@ -611,13 +611,13 @@ worker.onmessage = (event) => {
611
  const message = event.data || {};
612
  if (message.type === "model-progress") {
613
  const percent = Math.round(message.overall * 100);
614
- setStatus("Henter lokal model", `${percent}% · ${message.file}`);
615
  setProgress(message.overall);
616
  } else if (message.type === "ready") {
617
  state.ready = true;
618
  elements.recordButton.disabled = false;
619
  elements.recordLabel.textContent = "Klik for at tale";
620
- setStatus("Klar", `${formatBytes(message.bytes)} hentet`, "ready");
621
  setProgress(1, true);
622
  ensurePunctuation();
623
  } else if (message.type === "live-result") {
@@ -665,7 +665,7 @@ worker.onmessage = (event) => {
665
  pncWorker.onmessage = (event) => {
666
  const message = event.data || {};
667
  if (message.type === "pnc-progress") {
668
- elements.pncStatus.textContent = `Henter lokalt · ${Math.round(message.overall * 100)}%`;
669
  } else if (message.type === "pnc-ready") {
670
  state.pncReady = true;
671
  elements.pncStatus.textContent = `Klar · ${formatBytes(message.bytes)}`;
 
91
  level: 0,
92
  };
93
 
94
+ const worker = new Worker(signedAsset("worker.js?v=20260927-2"));
95
+ const pncWorker = new Worker(signedAsset("pnc-worker.js?v=20260927-2"));
96
 
97
  function setStatus(text, detail = "", kind = "loading") {
98
  elements.statusText.textContent = text;
 
611
  const message = event.data || {};
612
  if (message.type === "model-progress") {
613
  const percent = Math.round(message.overall * 100);
614
+ setStatus(message.cached ? "Indlæser gemt model" : "Indlæser lokal model", `${percent}% · ${message.file}`);
615
  setProgress(message.overall);
616
  } else if (message.type === "ready") {
617
  state.ready = true;
618
  elements.recordButton.disabled = false;
619
  elements.recordLabel.textContent = "Klik for at tale";
620
+ setStatus("Klar", `${formatBytes(message.bytes)} verificeret`, "ready");
621
  setProgress(1, true);
622
  ensurePunctuation();
623
  } else if (message.type === "live-result") {
 
665
  pncWorker.onmessage = (event) => {
666
  const message = event.data || {};
667
  if (message.type === "pnc-progress") {
668
+ elements.pncStatus.textContent = `${message.cached ? "Indlæser gemt model" : "Indlæser lokalt"} · ${Math.round(message.overall * 100)}%`;
669
  } else if (message.type === "pnc-ready") {
670
  state.pncReady = true;
671
  elements.pncStatus.textContent = `Klar · ${formatBytes(message.bytes)}`;
index.html CHANGED
@@ -195,6 +195,6 @@
195
  </form>
196
  </dialog>
197
 
198
- <script src="app.js?v=20260927-1" type="module"></script>
199
  </body>
200
  </html>
 
195
  </form>
196
  </dialog>
197
 
198
+ <script src="app.js?v=20260927-2" type="module"></script>
199
  </body>
200
  </html>
pnc-worker.js CHANGED
@@ -32,39 +32,51 @@ async function sha256(bytes) {
32
  }
33
 
34
  async function fetchVerified(file, completedBytes, totalBytes) {
35
- const response = await fetch(signedAsset(file.url), {cache: "force-cache"});
36
- if (!response.ok || !response.body) {
37
- throw new Error(`Kunne ikke hente ${file.path} (${response.status})`);
38
- }
39
- const reader = response.body.getReader();
40
- const chunks = [];
41
- let loaded = 0;
42
- while (true) {
43
- const {done, value} = await reader.read();
44
- if (done) break;
45
- chunks.push(value);
46
- loaded += value.byteLength;
47
- self.postMessage({
48
- type: "pnc-progress",
49
- file: file.path,
50
- loaded,
51
- fileBytes: file.bytes,
52
- overall: Math.min(1, (completedBytes + loaded) / totalBytes),
53
- });
54
- }
55
- const bytes = new Uint8Array(loaded);
56
- let offset = 0;
57
- for (const chunk of chunks) {
58
- bytes.set(chunk, offset);
59
- offset += chunk.byteLength;
60
- }
61
- if (loaded !== file.bytes) {
62
- throw new Error(`${file.path} har forkert størrelse (${loaded} bytes)`);
63
- }
64
- if (await sha256(bytes) !== file.sha256) {
65
- throw new Error(`${file.path} fejlede SHA-256-kontrol`);
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  }
67
- return bytes;
68
  }
69
 
70
  async function initialize(configUrl) {
 
32
  }
33
 
34
  async function fetchVerified(file, completedBytes, totalBytes) {
35
+ const url = signedAsset(file.url);
36
+ let cache = null;
37
+ try { cache = await self.caches?.open("ekko-browser-models-v1"); } catch { /* Storage is optional. */ }
38
+ for (let attempt = 0; attempt < 2; attempt += 1) {
39
+ let cached = null;
40
+ if (!attempt && cache) {
41
+ try { cached = await cache.match(url); } catch { /* Use the network. */ }
42
+ }
43
+ const response = cached || await fetch(url, {cache: attempt ? "reload" : "force-cache"});
44
+ if (!response.ok || !response.body) {
45
+ throw new Error(`Kunne ikke hente ${file.path} (${response.status})`);
46
+ }
47
+ const reader = response.body.getReader();
48
+ const bytes = new Uint8Array(file.bytes);
49
+ let loaded = 0;
50
+ while (true) {
51
+ const {done, value} = await reader.read();
52
+ if (done) break;
53
+ if (loaded + value.byteLength > bytes.length) {
54
+ await reader.cancel();
55
+ loaded = bytes.length + 1;
56
+ break;
57
+ }
58
+ bytes.set(value, loaded);
59
+ loaded += value.byteLength;
60
+ self.postMessage({
61
+ type: "pnc-progress",
62
+ cached: Boolean(cached),
63
+ file: file.path,
64
+ loaded,
65
+ fileBytes: file.bytes,
66
+ overall: Math.min(1, (completedBytes + loaded) / totalBytes),
67
+ });
68
+ }
69
+ if (loaded === file.bytes && await sha256(bytes) === file.sha256) {
70
+ if (!cached && cache) {
71
+ try { await cache.put(url, new Response(bytes)); } catch { /* Keep using the verified bytes. */ }
72
+ }
73
+ return bytes;
74
+ }
75
+ if (cached) {
76
+ try { await cache.delete(url); } catch { /* Retry without this entry. */ }
77
+ }
78
+ if (attempt) throw new Error(`${file.path} fejlede størrelses- eller SHA-256-kontrol`);
79
  }
 
80
  }
81
 
82
  async function initialize(configUrl) {
worker.js CHANGED
@@ -143,40 +143,51 @@ async function sha256(bytes) {
143
  }
144
 
145
  async function fetchVerified(file, completedBytes, totalBytes) {
146
- const response = await fetch(signedAsset(file.url), {cache: "force-cache"});
147
- if (!response.ok || !response.body) {
148
- throw new Error(`Kunne ikke hente ${file.path} (${response.status})`);
149
- }
150
- const reader = response.body.getReader();
151
- const chunks = [];
152
- let loaded = 0;
153
- while (true) {
154
- const {done, value} = await reader.read();
155
- if (done) break;
156
- chunks.push(value);
157
- loaded += value.byteLength;
158
- self.postMessage({
159
- type: "model-progress",
160
- file: file.path,
161
- loaded,
162
- fileBytes: file.bytes,
163
- overall: Math.min(1, (completedBytes + loaded) / totalBytes),
164
- });
165
- }
166
- const bytes = new Uint8Array(loaded);
167
- let offset = 0;
168
- for (const chunk of chunks) {
169
- bytes.set(chunk, offset);
170
- offset += chunk.byteLength;
171
- }
172
- if (loaded !== file.bytes) {
173
- throw new Error(`${file.path} har forkert størrelse (${loaded} bytes)`);
174
- }
175
- const digest = await sha256(bytes);
176
- if (digest !== file.sha256) {
177
- throw new Error(`${file.path} fejlede SHA-256-kontrol`);
 
 
 
 
 
 
 
 
 
 
 
 
178
  }
179
- return bytes;
180
  }
181
 
182
  function createRecognizer(runtimeConfig) {
 
143
  }
144
 
145
  async function fetchVerified(file, completedBytes, totalBytes) {
146
+ const url = signedAsset(file.url);
147
+ let cache = null;
148
+ try { cache = await self.caches?.open("ekko-browser-models-v1"); } catch { /* Storage is optional. */ }
149
+ for (let attempt = 0; attempt < 2; attempt += 1) {
150
+ let cached = null;
151
+ if (!attempt && cache) {
152
+ try { cached = await cache.match(url); } catch { /* Use the network. */ }
153
+ }
154
+ const response = cached || await fetch(url, {cache: attempt ? "reload" : "force-cache"});
155
+ if (!response.ok || !response.body) {
156
+ throw new Error(`Kunne ikke hente ${file.path} (${response.status})`);
157
+ }
158
+ const reader = response.body.getReader();
159
+ const bytes = new Uint8Array(file.bytes);
160
+ let loaded = 0;
161
+ while (true) {
162
+ const {done, value} = await reader.read();
163
+ if (done) break;
164
+ if (loaded + value.byteLength > bytes.length) {
165
+ await reader.cancel();
166
+ loaded = bytes.length + 1;
167
+ break;
168
+ }
169
+ bytes.set(value, loaded);
170
+ loaded += value.byteLength;
171
+ self.postMessage({
172
+ type: "model-progress",
173
+ cached: Boolean(cached),
174
+ file: file.path,
175
+ loaded,
176
+ fileBytes: file.bytes,
177
+ overall: Math.min(1, (completedBytes + loaded) / totalBytes),
178
+ });
179
+ }
180
+ if (loaded === file.bytes && await sha256(bytes) === file.sha256) {
181
+ if (!cached && cache) {
182
+ try { await cache.put(url, new Response(bytes)); } catch { /* Keep using the verified bytes. */ }
183
+ }
184
+ return bytes;
185
+ }
186
+ if (cached) {
187
+ try { await cache.delete(url); } catch { /* Retry without this entry. */ }
188
+ }
189
+ if (attempt) throw new Error(`${file.path} fejlede størrelses- eller SHA-256-kontrol`);
190
  }
 
191
  }
192
 
193
  function createRecognizer(runtimeConfig) {