Compare commits

...
4 Commits
Author SHA1 Message Date
Gitea Actions 557fc77a52 Release 0.1.39 [auto-version-bump] 2026-10-16 00:43:32 +02:00
Ein Anderssono d788ca39ec Version frontend assets and disable stale caching
Build and publish frontend / publish (push) Successful in 41s
2026-10-16 00:43:09 +02:00
Gitea Actions 00f7334580 Release 0.1.38 [auto-version-bump] 2026-10-16 00:41:18 +02:00
Ein Anderssono c5c0fb6b03 Offer speech retry after iOS playback errors
Build and publish frontend / publish (push) Successful in 43s
2026-10-16 00:40:52 +02:00
7 changed files with 26 additions and 8 deletions
+1 -1
View File
@@ -1 +1 @@
0.1.37 0.1.39
+2 -2
View File
@@ -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.37 version: 0.1.39
appVersion: "0.1.37" appVersion: "0.1.39"
+2 -2
View File
@@ -72,7 +72,7 @@
</section> </section>
</main> </main>
<footer id="version-footer">Frontend <span id="frontend-version">__FRONTEND_VERSION__</span> · API <span id="api-version">hämtar version…</span></footer> <footer id="version-footer">Frontend <span id="frontend-version">__FRONTEND_VERSION__</span> · API <span id="api-version">hämtar version…</span></footer>
<script src="/voice.js"></script> <script src="/voice.js?v=__FRONTEND_VERSION__"></script>
<script> <script>
const auth = document.querySelector('#auth'); const auth = document.querySelector('#auth');
const play = document.querySelector('#play'); const play = document.querySelector('#play');
@@ -219,7 +219,7 @@
microphone.textContent = listening ? 'Avsluta mikrofon' : 'Starta mikrofon'; microphone.textContent = listening ? 'Avsluta mikrofon' : 'Starta mikrofon';
refreshMicrophone(); refreshMicrophone();
if (message) setVoiceStatus(message); if (message) setVoiceStatus(message);
if (/Uppläsningen startade inte/.test(message)) showSpeechRecovery(message); if (/Läs senaste repliken/.test(message)) showSpeechRecovery(message);
playVoiceSignal(state === 'IDLE' && /Mikrofonen är avstängd/.test(message) ? 'STOPPED' : state); playVoiceSignal(state === 'IDLE' && /Mikrofonen är avstängd/.test(message) ? 'STOPPED' : state);
}, },
onTranscript(text) { onTranscript(text) {
+3 -1
View File
@@ -10,7 +10,9 @@ server {
} }
location / { location / {
add_header Cache-Control "no-cache" always; add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
add_header Pragma "no-cache" always;
add_header Expires "0" always;
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
} }
+2
View File
@@ -4,6 +4,8 @@ set -euo pipefail
test -s index.html test -s index.html
grep -q '<html lang="sv">' index.html grep -q '<html lang="sv">' index.html
grep -q 'id="frontend-version">__FRONTEND_VERSION__' index.html grep -q 'id="frontend-version">__FRONTEND_VERSION__' index.html
grep -q '<script src="/voice.js?v=__FRONTEND_VERSION__"></script>' index.html
grep -q 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0' nginx/default.conf
grep -q "request('/version')" index.html grep -q "request('/version')" index.html
grep -Fq "credentials: 'same-origin'" index.html grep -Fq "credentials: 'same-origin'" index.html
grep -q 'id="dialogue" aria-label="Dialoghistorik" role="log" aria-live="polite"' index.html grep -q 'id="dialogue" aria-label="Dialoghistorik" role="log" aria-live="polite"' index.html
+10
View File
@@ -165,6 +165,16 @@ test('speech start timeout tells the player how to recover when mobile playback
assert.equal(h.synthesis.cancelled, cancelledBeforeRetry); assert.equal(h.synthesis.cancelled, cancelledBeforeRetry);
}); });
test('iOS not-allowed speech error offers an immediate gesture-based retry', () => {
const h = makeHarness();
h.controller.speak('En replik');
h.utterances[0].onerror({error: 'not-allowed'});
assert.match(h.states.at(-1).message, /Safari blockerade uppläsningen/);
assert.match(h.states.at(-1).message, /Läs senaste repliken/);
assert.equal(h.controller.speak('Försök igen'), true);
assert.equal(h.synthesis.spoken.at(-1).text, 'Försök igen');
});
test('character voice identity gives stable distinct masculine, feminine, and neutral timbres', () => { test('character voice identity gives stable distinct masculine, feminine, and neutral timbres', () => {
const profile = GameVoice.characterVoiceProfile; const profile = GameVoice.characterVoiceProfile;
const masculine = profile('masculine', 'character-1'); const masculine = profile('masculine', 'character-1');
+6 -2
View File
@@ -240,7 +240,7 @@
currentUtterance = null; currentUtterance = null;
setState('IDLE'); setState('IDLE');
}; };
utterance.onerror = () => { utterance.onerror = event => {
if (token !== generation) return; if (token !== generation) return;
if (speechTimer !== null) win.clearTimeout(speechTimer); if (speechTimer !== null) win.clearTimeout(speechTimer);
if (speechStartTimer !== null) win.clearTimeout(speechStartTimer); if (speechStartTimer !== null) win.clearTimeout(speechStartTimer);
@@ -248,7 +248,11 @@
speechStartTimer = null; speechStartTimer = null;
currentUtterance = null; currentUtterance = null;
generation += 1; generation += 1;
setState('IDLE', 'Uppläsningen avbröts. Dialogen finns kvar som text.'); const blocked = event?.error === 'not-allowed' || event?.error === 'service-not-allowed';
const message = blocked
? 'Safari blockerade uppläsningen. Tryck på Läs senaste repliken för att starta ljudet.'
: 'Uppläsningen avbröts. Tryck på Läs senaste repliken för att försöka igen. Dialogen finns kvar som text.';
setState('IDLE', message);
}; };
setState('SPEAKING', 'Läser upp dialog.'); setState('SPEAKING', 'Läser upp dialog.');
currentUtterance = utterance; currentUtterance = utterance;