Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e7c74bb41 | ||
|
|
89c238bcea |
@@ -2,5 +2,5 @@ apiVersion: v2
|
|||||||
name: ai-spel-frontend
|
name: ai-spel-frontend
|
||||||
description: Static web frontend for the Swedish life simulator
|
description: Static web frontend for the Swedish life simulator
|
||||||
type: application
|
type: application
|
||||||
version: 0.1.55
|
version: 0.1.56
|
||||||
appVersion: "0.1.55"
|
appVersion: "0.1.56"
|
||||||
+11
-6
@@ -170,19 +170,24 @@
|
|||||||
const value = document.querySelector('#speech-progress-value');
|
const value = document.querySelector('#speech-progress-value');
|
||||||
const label = container.querySelector('label');
|
const label = container.querySelector('label');
|
||||||
const preparing = message.startsWith('Förbereder svensk uppläsning.');
|
const preparing = message.startsWith('Förbereder svensk uppläsning.');
|
||||||
const starting = message.startsWith('Startar svensk röstmotor och förbereder modellen.');
|
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.',
|
||||||
|
'Röstmodellen är inläst. Skapar uppläsningen.',
|
||||||
|
].find(text => message.startsWith(text));
|
||||||
const match = message.match(/^Hämtar svensk röstmodell till enheten: (\d+) procent\.$/);
|
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\.$/);
|
const bytesMatch = message.match(/^Hämtar svensk röstmodell till enheten: ([\d.,]+) MB hämtade\.$/);
|
||||||
if (!preparing && !starting && !match && !bytesMatch) {
|
if (!preparing && !phase && !match && !bytesMatch) {
|
||||||
container.hidden = true;
|
container.hidden = true;
|
||||||
progress.removeAttribute('value');
|
progress.removeAttribute('value');
|
||||||
value.textContent = 'Väntar på nedladdning';
|
value.textContent = 'Väntar på nedladdning';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
container.hidden = false;
|
container.hidden = false;
|
||||||
label.textContent = starting
|
label.textContent = phase || 'Hämtar svensk röstmodell till enheten';
|
||||||
? 'Startar och läser in den svenska röstmodellen'
|
|
||||||
: 'Hämtar svensk röstmodell till enheten';
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const percent = Math.max(0, Math.min(100, Number(match[1])));
|
const percent = Math.max(0, Math.min(100, Number(match[1])));
|
||||||
progress.value = percent;
|
progress.value = percent;
|
||||||
@@ -195,7 +200,7 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
progress.removeAttribute('value');
|
progress.removeAttribute('value');
|
||||||
value.textContent = starting ? 'Startar röstmotorn…' : 'Förbereder…';
|
value.textContent = phase ? 'Pågår…' : 'Förbereder…';
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
function clearSpeechRecovery() {
|
function clearSpeechRecovery() {
|
||||||
|
|||||||
+16
-3
@@ -7,15 +7,27 @@ const VOICE_ID = 'sv_SE-nst-medium';
|
|||||||
const ASSET_ROOT = '/static/piper-wasm-v1';
|
const ASSET_ROOT = '/static/piper-wasm-v1';
|
||||||
const localFetch = self.fetch.bind(self);
|
const localFetch = self.fetch.bind(self);
|
||||||
|
|
||||||
|
function reportStage(id, stage) {
|
||||||
|
if (Number.isSafeInteger(id)) self.postMessage({id, type: 'progress', progress: {stage}});
|
||||||
|
}
|
||||||
|
|
||||||
// Keep the TTS library's OPFS cache logic, but serve models/config from the
|
// Keep the TTS library's OPFS cache logic, but serve models/config from the
|
||||||
// same-origin static asset service instead of its built-in external model URL.
|
// same-origin static asset service instead of its built-in external model URL.
|
||||||
self.fetch = async (resource, options) => {
|
self.fetch = async (resource, options) => {
|
||||||
const requestedUrl = resource instanceof Request ? resource.url : String(resource);
|
const requestedUrl = resource instanceof Request ? resource.url : String(resource);
|
||||||
const localModelUrl = localPiperModelUrl(requestedUrl, self.location.origin);
|
const localModelUrl = localPiperModelUrl(requestedUrl, self.location.origin);
|
||||||
if (!localModelUrl) return localFetch(resource, options);
|
if (!localModelUrl) {
|
||||||
|
if (activeRequestId && requestedUrl.includes('/static/piper-wasm-v1/onnx/')) {
|
||||||
|
reportStage(activeRequestId, 'loading-runtime-assets');
|
||||||
|
}
|
||||||
|
return localFetch(resource, options);
|
||||||
|
}
|
||||||
|
const isModel = localModelUrl.endsWith('.onnx');
|
||||||
|
if (activeRequestId) reportStage(activeRequestId, isModel ? 'loading-model' : 'loading-model-info');
|
||||||
const redirected = resource instanceof Request ? new Request(localModelUrl, resource) : localModelUrl;
|
const redirected = resource instanceof Request ? new Request(localModelUrl, resource) : localModelUrl;
|
||||||
const response = await localFetch(redirected, options);
|
const response = await localFetch(redirected, options);
|
||||||
if (!activeRequestId || !localModelUrl.endsWith('.onnx')) return response;
|
if (!activeRequestId || !isModel) return response;
|
||||||
|
reportStage(activeRequestId, 'reading-model');
|
||||||
return trackModelDownload(response, {
|
return trackModelDownload(response, {
|
||||||
id: activeRequestId,
|
id: activeRequestId,
|
||||||
postMessage: message => self.postMessage(message),
|
postMessage: message => self.postMessage(message),
|
||||||
@@ -47,9 +59,10 @@ async function synthesizeMessage(event) {
|
|||||||
const {id, text} = event.data || {};
|
const {id, text} = event.data || {};
|
||||||
if (!Number.isSafeInteger(id) || typeof text !== 'string') return;
|
if (!Number.isSafeInteger(id) || typeof text !== 'string') return;
|
||||||
activeRequestId = id;
|
activeRequestId = id;
|
||||||
self.postMessage({id, type: 'progress', progress: {stage: 'starting'}});
|
reportStage(id, 'loading-runtime');
|
||||||
try {
|
try {
|
||||||
const session = await getSession();
|
const session = await getSession();
|
||||||
|
reportStage(id, 'model-ready');
|
||||||
const blob = await session.predict(text);
|
const blob = await session.predict(text);
|
||||||
const audio = await blob.arrayBuffer();
|
const audio = await blob.arrayBuffer();
|
||||||
self.postMessage({id, type: 'audio', audio}, [audio]);
|
self.postMessage({id, type: 'audio', audio}, [audio]);
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ grep -q 'id="voice-status" role="status" aria-live="polite"' index.html
|
|||||||
grep -q 'id="speech-progress" aria-label="Nedladdning av svensk röstmodell"' index.html
|
grep -q 'id="speech-progress" aria-label="Nedladdning av svensk röstmodell"' index.html
|
||||||
grep -q 'id="speech-progress-value"' index.html
|
grep -q 'id="speech-progress-value"' index.html
|
||||||
grep -q 'MB hämtade' index.html
|
grep -q 'MB hämtade' index.html
|
||||||
|
grep -q 'loading-runtime-assets' piper-worker.js
|
||||||
|
grep -q 'Röstmodellen är inläst' voice.js
|
||||||
grep -q "window.GameVoice.createVoiceController" index.html
|
grep -q "window.GameVoice.createVoiceController" index.html
|
||||||
grep -q "addVoiceButton(item, 'Testa ljudet'" index.html
|
grep -q "addVoiceButton(item, 'Testa ljudet'" index.html
|
||||||
grep -q 'Jag förstår – starta mikrofonen' index.html
|
grep -q 'Jag förstår – starta mikrofonen' index.html
|
||||||
|
|||||||
@@ -259,6 +259,8 @@ test('Piper model download progress is reported as a bounded percentage', async
|
|||||||
h.controller.activateFromUserGesture();
|
h.controller.activateFromUserGesture();
|
||||||
h.controller.speak('Ett svar från Piper.');
|
h.controller.speak('Ett svar från Piper.');
|
||||||
await new Promise(resolve => setImmediate(resolve));
|
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]({loaded: 31_500_000, total: 63_000_000});
|
h.piperProgressCallbacks[0]({loaded: 31_500_000, total: 63_000_000});
|
||||||
assert.equal(h.states.at(-1).state, 'PROCESSING');
|
assert.equal(h.states.at(-1).state, 'PROCESSING');
|
||||||
assert.match(h.states.at(-1).message, /50 procent/);
|
assert.match(h.states.at(-1).message, /50 procent/);
|
||||||
|
|||||||
@@ -254,8 +254,16 @@
|
|||||||
let lastProgressMegabytes = -1;
|
let lastProgressMegabytes = -1;
|
||||||
const progress = event => {
|
const progress = event => {
|
||||||
if (token !== generation) return;
|
if (token !== generation) return;
|
||||||
if (event?.stage === 'starting') {
|
const stages = {
|
||||||
setState('PROCESSING', 'Startar svensk röstmotor och förbereder modellen.');
|
'loading-runtime': 'Laddar WebAssembly-motorn för svensk uppläsning.',
|
||||||
|
'loading-runtime-assets': 'Laddar WebAssembly-filer för svensk uppläsning.',
|
||||||
|
'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.',
|
||||||
|
'model-ready': 'Röstmodellen är inläst. Skapar uppläsningen.',
|
||||||
|
};
|
||||||
|
if (stages[event?.stage]) {
|
||||||
|
setState('PROCESSING', stages[event.stage]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!Number.isFinite(event?.loaded) || event.loaded < 0) return;
|
if (!Number.isFinite(event?.loaded) || event.loaded < 0) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user