Add spelling improvements and local Piper TTS

Practice: fire streak badge, first-letter hint on wrong answers, missed-words drill session after practice completes.
Test: audio-only mode with listening state before input appears, larger re-read button.
Words: definition field (DB migration + admin editor + TTS playback).
TTS: replace speechSynthesis with local Piper via /api/tts, WAV cached in data/tts-cache, falls back to browser voice when Piper is not installed.
Dockerfile: switched to node:26-slim, installs Piper binary + en_US-lessac-medium voice.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Nate EmmertandClaude Sonnet 4.6 committed 2026-07-21 08:47:10 -04:00
1 parent 544469d6c9
commit 6f2281e2a7
8 files changed
+202 -35

No files matched your search

+32
View File
@@ -141,6 +141,34 @@ body {
font-size: 2.6rem; font-weight: 800; letter-spacing: 6px; margin: 0.5rem 0;
}
.countdown { color: var(--muted); }
.hint-badge { font-size: 1rem; color: var(--accent-dark); margin: 0.3rem 0 0.5rem; }
.streak-badge {
display: inline-block; margin-left: 0.5rem;
background: #fff3cd; color: #7c5200;
border-radius: 999px; padding: 0.1rem 0.7rem;
font-size: 0.95rem; font-weight: 700;
animation: streak-pop 0.3s ease;
}
@keyframes streak-pop {
0% { transform: scale(0.7); opacity: 0; }
70% { transform: scale(1.15); }
100% { transform: scale(1); opacity: 1; }
}
.test-listening-state {
font-size: 1.5rem; font-weight: 700; color: var(--accent);
text-align: center; padding: 2rem 0; letter-spacing: 1px;
animation: pulse 1s ease-in-out infinite alternate;
}
@keyframes pulse {
from { opacity: 0.6; }
to { opacity: 1; }
}
.speak-btn-prominent {
font-size: 1.4rem; padding: 0.75rem 2rem; width: 100%; max-width: 340px;
display: block; margin: 0 auto 1.25rem;
}
.done-card {
background: var(--card); border-radius: var(--radius);
@@ -154,6 +182,10 @@ body {
font-size: 1.2rem; font-weight: 700; font-family: inherit;
background: var(--accent); color: #fff; border: none;
border-radius: 12px; padding: 0.7rem 2rem; cursor: pointer; margin-top: 1rem;
display: block; width: 100%;
}
.done-card button.secondary {
background: var(--good-bg); color: var(--good); border: 2px solid var(--good);
}
/* ---------- parent dashboard ---------- */