Fix TTS audio stalling and add back/previous-word navigation in spelling
Build and Push Docker Image / build-and-push (push) Successful in 55s
Build and Push Docker Image / build-and-push (push) Successful in 55s
- Add timeout fallback in ttsDrain so the queue never permanently stalls when the browser fails to fire onend (common Chrome/Safari bug after extended use) - Call speechSynthesis.resume() before speak() to un-stick suspended synthesis on mobile - Rename "← Stop" / "← Cancel" to "← Back" on practice and test views - Add "← Previous word" button in spelling test so kids can go back one word (e.g. when audio broke) — pops the saved answer and replays Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
74c4530060
commit
6baea15986
2 files changed
+26
-7
No files matched your search
+23
-5
@@ -122,22 +122,30 @@ function ttsCancel() {
|
||||
async function ttsDrain() {
|
||||
if (!ttsQueue.length) { ttsCurrentAudio = null; return; }
|
||||
const text = ttsQueue.shift();
|
||||
// Fallback timeout in case browser never fires onend (common speechSynthesis bug)
|
||||
const timeoutMs = Math.max(4000, text.length * 80);
|
||||
|
||||
if (piperAvailable) {
|
||||
const audio = new Audio(`/api/tts?text=${encodeURIComponent(text)}`);
|
||||
ttsCurrentAudio = audio;
|
||||
await new Promise((resolve) => {
|
||||
audio.onended = resolve;
|
||||
audio.onerror = resolve;
|
||||
audio.play().catch(resolve);
|
||||
let t = setTimeout(resolve, timeoutMs);
|
||||
const done = () => { clearTimeout(t); resolve(); };
|
||||
audio.onended = done;
|
||||
audio.onerror = done;
|
||||
audio.play().catch(done);
|
||||
});
|
||||
} else {
|
||||
const u = new SpeechSynthesisUtterance(text);
|
||||
u.rate = 0.85; u.lang = 'en-US';
|
||||
speechSynthesis.cancel();
|
||||
await new Promise((resolve) => {
|
||||
u.onend = resolve;
|
||||
u.onerror = resolve;
|
||||
let t = setTimeout(resolve, timeoutMs);
|
||||
const done = () => { clearTimeout(t); resolve(); };
|
||||
u.onend = done;
|
||||
u.onerror = done;
|
||||
// Un-stick suspended synthesis (common on mobile after a few minutes)
|
||||
if (speechSynthesis.paused) speechSynthesis.resume();
|
||||
// some browsers need a short delay after cancel() before speak() works
|
||||
setTimeout(() => speechSynthesis.speak(u), 50);
|
||||
});
|
||||
@@ -2386,6 +2394,7 @@ function presentTestWord() {
|
||||
input.disabled = true;
|
||||
$('#test-listening').hidden = false;
|
||||
$('#test-form-row').hidden = true;
|
||||
$('#btn-test-prev').hidden = test.i === 0;
|
||||
speakWord(w);
|
||||
// Enable input after TTS has had time to start (rough estimate: 1.5s per word)
|
||||
const delay = 1500 + (w.definition ? 1500 : 0) + (w.sentence ? 1500 : 0);
|
||||
@@ -2397,6 +2406,15 @@ function presentTestWord() {
|
||||
}, delay);
|
||||
}
|
||||
|
||||
$('#btn-test-prev').addEventListener('click', () => {
|
||||
if (test.i > 0) {
|
||||
ttsCancel();
|
||||
test.answers.pop();
|
||||
test.i--;
|
||||
presentTestWord();
|
||||
}
|
||||
});
|
||||
|
||||
$('#btn-test-speak').addEventListener('click', () => speakWord(test.words[test.i]));
|
||||
|
||||
$('#test-form').addEventListener('submit', async (e) => {
|
||||
|
||||
+3
-2
@@ -245,7 +245,7 @@
|
||||
|
||||
<!-- Practice (spelling) -->
|
||||
<section id="view-practice" class="view" hidden>
|
||||
<button class="back-btn" data-back="practice">← Stop</button>
|
||||
<button class="back-btn" data-back="practice">← Back</button>
|
||||
<p id="practice-progress" class="progress-label"></p>
|
||||
<div class="progress-track"><div id="practice-bar" class="progress-fill"></div></div>
|
||||
|
||||
@@ -265,7 +265,7 @@
|
||||
|
||||
<!-- Friday test (spelling) -->
|
||||
<section id="view-test" class="view" hidden>
|
||||
<button class="back-btn" data-back="test">← Cancel</button>
|
||||
<button class="back-btn" data-back="test">← Back</button>
|
||||
<p id="test-progress" class="progress-label"></p>
|
||||
<div class="progress-track"><div id="test-bar" class="progress-fill test-fill"></div></div>
|
||||
|
||||
@@ -279,6 +279,7 @@
|
||||
autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="Spell the word">
|
||||
<button type="submit" class="check-btn">Next →</button>
|
||||
</form>
|
||||
<button id="btn-test-prev" class="speak-btn" style="margin-top:.75rem" hidden>← Previous word</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user