Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa832fd7cc | ||
|
|
d788f8b350 | ||
|
|
04bac2ba30 |
@@ -2,5 +2,5 @@ apiVersion: v2
|
||||
name: ai-spel-frontend
|
||||
description: Static web frontend for the Swedish life simulator
|
||||
type: application
|
||||
version: 0.1.56
|
||||
appVersion: "0.1.56"
|
||||
version: 0.1.57
|
||||
appVersion: "0.1.57"
|
||||
+19
-3
@@ -172,7 +172,6 @@
|
||||
const preparing = message.startsWith('Förbereder svensk uppläsning.');
|
||||
const phase = [
|
||||
'Laddar WebAssembly-motorn för svensk uppläsning.',
|
||||
'Laddar WebAssembly-filer för svensk uppläsning.',
|
||||
'Hämtar information om den svenska röstmodellen.',
|
||||
'Ansluter till servern för att hämta den svenska röstmodellen.',
|
||||
'Läser in den svenska röstmodellen.',
|
||||
@@ -180,14 +179,31 @@
|
||||
].find(text => message.startsWith(text));
|
||||
const match = message.match(/^Hämtar svensk röstmodell till enheten: (\d+) procent\.$/);
|
||||
const bytesMatch = message.match(/^Hämtar svensk röstmodell till enheten: ([\d.,]+) MB hämtade\.$/);
|
||||
if (!preparing && !phase && !match && !bytesMatch) {
|
||||
const runtimeFile = message.match(/^(?:Hämtar|Läser in) WebAssembly-filen ([\w.-]+)\.?$/);
|
||||
const runtimePercent = message.match(/^Hämtar WebAssembly-filen ([\w.-]+): (\d+) procent\.$/);
|
||||
const runtimeBytes = message.match(/^Hämtar WebAssembly-filen ([\w.-]+): ([\d.,]+) MB hämtade\.$/);
|
||||
if (!preparing && !phase && !match && !bytesMatch && !runtimeFile && !runtimePercent && !runtimeBytes) {
|
||||
container.hidden = true;
|
||||
progress.removeAttribute('value');
|
||||
value.textContent = 'Väntar på nedladdning';
|
||||
return false;
|
||||
}
|
||||
container.hidden = false;
|
||||
label.textContent = phase || 'Hämtar svensk röstmodell till enheten';
|
||||
label.textContent = runtimePercent || runtimeBytes
|
||||
? `Hämtar WebAssembly-filen ${runtimePercent?.[1] || runtimeBytes?.[1]}`
|
||||
: runtimeFile
|
||||
? runtimeFile[0].replace(/\.$/, '')
|
||||
: phase || 'Hämtar svensk röstmodell till enheten';
|
||||
if (runtimePercent) {
|
||||
progress.value = Math.max(0, Math.min(100, Number(runtimePercent[2])));
|
||||
value.textContent = `${runtimePercent[2]}%`;
|
||||
return true;
|
||||
}
|
||||
if (runtimeBytes) {
|
||||
progress.removeAttribute('value');
|
||||
value.textContent = `${runtimeBytes[2]} MB hämtade`;
|
||||
return true;
|
||||
}
|
||||
if (match) {
|
||||
const percent = Math.max(0, Math.min(100, Number(match[1])));
|
||||
progress.value = percent;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export function trackModelDownload(response, {id, postMessage}) {
|
||||
export function trackDownloadProgress(response, {id, postMessage, asset = 'model', file}) {
|
||||
const reader = response.body?.getReader?.();
|
||||
if (!reader || typeof ReadableStream !== 'function' || typeof Response !== 'function') return response;
|
||||
|
||||
@@ -13,7 +13,9 @@ export function trackModelDownload(response, {id, postMessage}) {
|
||||
return;
|
||||
}
|
||||
loaded += value.byteLength;
|
||||
postMessage({id, type: 'progress', progress: {loaded, total}});
|
||||
const progress = {asset, loaded, total};
|
||||
if (file) progress.file = file;
|
||||
postMessage({id, type: 'progress', progress});
|
||||
controller.enqueue(value);
|
||||
} catch (error) {
|
||||
controller.error(error);
|
||||
|
||||
+17
-7
@@ -1,5 +1,5 @@
|
||||
import {TtsSession} from '@mintplex-labs/piper-tts-web';
|
||||
import {trackModelDownload} from './piper-download-progress.mjs';
|
||||
import {trackDownloadProgress} from './piper-download-progress.mjs';
|
||||
import {localPiperModelUrl} from './piper-model-routing.mjs';
|
||||
import {createOnnxWasmPaths} from './piper-onnx-config.mjs';
|
||||
|
||||
@@ -7,8 +7,8 @@ const VOICE_ID = 'sv_SE-nst-medium';
|
||||
const ASSET_ROOT = '/static/piper-wasm-v1';
|
||||
const localFetch = self.fetch.bind(self);
|
||||
|
||||
function reportStage(id, stage) {
|
||||
if (Number.isSafeInteger(id)) self.postMessage({id, type: 'progress', progress: {stage}});
|
||||
function reportStage(id, stage, details = {}) {
|
||||
if (Number.isSafeInteger(id)) self.postMessage({id, type: 'progress', progress: {stage, ...details}});
|
||||
}
|
||||
|
||||
// Keep the TTS library's OPFS cache logic, but serve models/config from the
|
||||
@@ -17,10 +17,20 @@ self.fetch = async (resource, options) => {
|
||||
const requestedUrl = resource instanceof Request ? resource.url : String(resource);
|
||||
const localModelUrl = localPiperModelUrl(requestedUrl, self.location.origin);
|
||||
if (!localModelUrl) {
|
||||
if (activeRequestId && requestedUrl.includes('/static/piper-wasm-v1/onnx/')) {
|
||||
reportStage(activeRequestId, 'loading-runtime-assets');
|
||||
const runtimeAsset = requestedUrl.includes('/static/piper-wasm-v1/onnx/');
|
||||
const runtimeFile = runtimeAsset ? new URL(requestedUrl, self.location.origin).pathname.split('/').pop() : undefined;
|
||||
if (activeRequestId && runtimeAsset) {
|
||||
reportStage(activeRequestId, 'loading-runtime-assets', {file: runtimeFile});
|
||||
}
|
||||
return localFetch(resource, options);
|
||||
const response = await localFetch(resource, options);
|
||||
if (!activeRequestId || !runtimeAsset) return response;
|
||||
reportStage(activeRequestId, 'reading-runtime-assets', {file: runtimeFile});
|
||||
return trackDownloadProgress(response, {
|
||||
id: activeRequestId,
|
||||
asset: 'runtime',
|
||||
file: runtimeFile,
|
||||
postMessage: message => self.postMessage(message),
|
||||
});
|
||||
}
|
||||
const isModel = localModelUrl.endsWith('.onnx');
|
||||
if (activeRequestId) reportStage(activeRequestId, isModel ? 'loading-model' : 'loading-model-info');
|
||||
@@ -28,7 +38,7 @@ self.fetch = async (resource, options) => {
|
||||
const response = await localFetch(redirected, options);
|
||||
if (!activeRequestId || !isModel) return response;
|
||||
reportStage(activeRequestId, 'reading-model');
|
||||
return trackModelDownload(response, {
|
||||
return trackDownloadProgress(response, {
|
||||
id: activeRequestId,
|
||||
postMessage: message => self.postMessage(message),
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@ grep -q "new Worker(workerUrl, {type: 'module'})" piper-runtime.js
|
||||
grep -q 'worker.js?v=' piper-runtime.js
|
||||
grep -q 'worker.onmessageerror' piper-runtime.js
|
||||
grep -q 'localPiperModelUrl' piper-worker.js
|
||||
grep -q 'trackModelDownload' piper-worker.js
|
||||
grep -q 'trackDownloadProgress' piper-worker.js
|
||||
grep -q 'new ReadableStream' piper-download-progress.mjs
|
||||
grep -q 'createOnnxWasmPaths(self.location.origin)' piper-worker.js
|
||||
grep -q '/static/piper-wasm-v1/onnx/' piper-onnx-config.mjs
|
||||
|
||||
@@ -2,7 +2,7 @@ const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
test('model response progress forwards each downloaded chunk and preserves the response', async () => {
|
||||
const {trackModelDownload} = await import('../piper-download-progress.mjs');
|
||||
const {trackDownloadProgress} = await import('../piper-download-progress.mjs');
|
||||
const source = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new Uint8Array([1, 2]));
|
||||
@@ -12,22 +12,27 @@ test('model response progress forwards each downloaded chunk and preserves the r
|
||||
});
|
||||
const response = new Response(source, {headers: {'content-length': '5'}});
|
||||
const messages = [];
|
||||
const tracked = trackModelDownload(response, {id: 7, postMessage: message => messages.push(message)});
|
||||
const tracked = trackDownloadProgress(response, {id: 7, postMessage: message => messages.push(message)});
|
||||
|
||||
assert.deepEqual([...new Uint8Array(await tracked.arrayBuffer())], [1, 2, 3, 4, 5]);
|
||||
assert.deepEqual(messages.map(message => message.progress), [
|
||||
{loaded: 2, total: 5},
|
||||
{loaded: 5, total: 5},
|
||||
{asset: 'model', loaded: 2, total: 5},
|
||||
{asset: 'model', loaded: 5, total: 5},
|
||||
]);
|
||||
assert.ok(messages.every(message => message.id === 7 && message.type === 'progress'));
|
||||
});
|
||||
|
||||
test('model response progress works when content length is absent', async () => {
|
||||
const {trackModelDownload} = await import('../piper-download-progress.mjs');
|
||||
const {trackDownloadProgress} = await import('../piper-download-progress.mjs');
|
||||
const source = new Response(new Uint8Array([1, 2, 3]));
|
||||
const messages = [];
|
||||
const tracked = trackModelDownload(source, {id: 9, postMessage: message => messages.push(message)});
|
||||
const tracked = trackDownloadProgress(source, {
|
||||
id: 9,
|
||||
asset: 'runtime',
|
||||
file: 'runtime.wasm',
|
||||
postMessage: message => messages.push(message),
|
||||
});
|
||||
|
||||
assert.equal((await tracked.arrayBuffer()).byteLength, 3);
|
||||
assert.deepEqual(messages.map(message => message.progress), [{loaded: 3, total: 0}]);
|
||||
assert.deepEqual(messages.map(message => message.progress), [{asset: 'runtime', file: 'runtime.wasm', loaded: 3, total: 0}]);
|
||||
});
|
||||
|
||||
@@ -261,6 +261,8 @@ test('Piper model download progress is reported as a bounded percentage', async
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
h.piperProgressCallbacks[0]({stage: 'loading-model'});
|
||||
assert.match(h.states.at(-1).message, /Ansluter till servern/);
|
||||
h.piperProgressCallbacks[0]({asset: 'runtime', file: 'ort-wasm-simd-threaded.wasm', loaded: 5 * 1024 * 1024, total: 10 * 1024 * 1024});
|
||||
assert.match(h.states.at(-1).message, /ort-wasm-simd-threaded\.wasm: 50 procent/);
|
||||
h.piperProgressCallbacks[0]({loaded: 31_500_000, total: 63_000_000});
|
||||
assert.equal(h.states.at(-1).state, 'PROCESSING');
|
||||
assert.match(h.states.at(-1).message, /50 procent/);
|
||||
|
||||
@@ -252,11 +252,34 @@
|
||||
|
||||
let lastProgressPercent = -1;
|
||||
let lastProgressMegabytes = -1;
|
||||
let lastProgressResource = '';
|
||||
const progress = event => {
|
||||
if (token !== generation) return;
|
||||
const resource = `${event?.asset || 'model'}:${event?.file || ''}`;
|
||||
if (resource !== lastProgressResource) {
|
||||
lastProgressResource = resource;
|
||||
lastProgressPercent = -1;
|
||||
lastProgressMegabytes = -1;
|
||||
}
|
||||
if (event?.asset === 'runtime' && Number.isFinite(event.loaded) && event.loaded >= 0) {
|
||||
const file = /^[\w.-]+$/.test(event.file || '') ? event.file : 'WebAssembly-fil';
|
||||
if (Number.isFinite(event.total) && event.total > 0) {
|
||||
const percent = Math.max(0, Math.min(100, Math.floor((event.loaded / event.total) * 100)));
|
||||
if (percent === lastProgressPercent) return;
|
||||
lastProgressPercent = percent;
|
||||
setState('PROCESSING', `Hämtar WebAssembly-filen ${file}: ${percent} procent.`);
|
||||
} else {
|
||||
const megabytes = Math.floor((event.loaded / (1024 * 1024)) * 10) / 10;
|
||||
if (megabytes === lastProgressMegabytes) return;
|
||||
lastProgressMegabytes = megabytes;
|
||||
setState('PROCESSING', `Hämtar WebAssembly-filen ${file}: ${megabytes.toLocaleString('sv-SE')} MB hämtade.`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const stages = {
|
||||
'loading-runtime': 'Laddar WebAssembly-motorn för svensk uppläsning.',
|
||||
'loading-runtime-assets': 'Laddar WebAssembly-filer för svensk uppläsning.',
|
||||
'loading-runtime-assets': `Hämtar WebAssembly-filen ${event?.file || 'från servern'}.`,
|
||||
'reading-runtime-assets': `Läser in WebAssembly-filen ${event?.file || ''}.`,
|
||||
'loading-model-info': 'Hämtar information om den svenska röstmodellen.',
|
||||
'loading-model': 'Ansluter till servern för att hämta den svenska röstmodellen.',
|
||||
'reading-model': 'Läser in den svenska röstmodellen.',
|
||||
|
||||
Reference in New Issue
Block a user