← back to Cncp Failures Mockups
v7 voices: 8 per-band TTS profiles (voice/pitch/rate) via Web Speech API; greeting on band change, blurbs in modal open, toast narration; mute toggle
3ab046b2b87d64c103b3d8ceebd464992690a829 · 2026-05-11 16:12:18 -0700 · SteveStudio2
Files touched
M public/v7-age-adaptive.html
Diff
commit 3ab046b2b87d64c103b3d8ceebd464992690a829
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Mon May 11 16:12:18 2026 -0700
v7 voices: 8 per-band TTS profiles (voice/pitch/rate) via Web Speech API; greeting on band change, blurbs in modal open, toast narration; mute toggle
---
public/v7-age-adaptive.html | 94 +++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 91 insertions(+), 3 deletions(-)
diff --git a/public/v7-age-adaptive.html b/public/v7-age-adaptive.html
index 7ab906d..7bfbd82 100644
--- a/public/v7-age-adaptive.html
+++ b/public/v7-age-adaptive.html
@@ -182,6 +182,7 @@
<div class="age-band-label" id="bandLabel">toddler</div>
</div>
<input id="ageSlider" type="range" min="3" max="95" value="3" />
+ <button id="voiceToggle" class="pill-btn" onclick="toggleVoice()" title="Toggle voice narration" style="margin-left:auto;">🔊 voice on</button>
<div class="tagline" id="tagline">Primary colors, huge taps. One face = good. One face = sad.</div>
<div class="pills" id="bandPills"></div>
</div>
@@ -228,6 +229,7 @@ const bandLabel = document.getElementById('bandLabel');
const tagline = document.getElementById('tagline');
const hed = document.getElementById('hed');
+let _lastSpokenBand = null;
function applyBand(age) {
const b = bandFor(age);
document.body.dataset.band = b.id;
@@ -237,6 +239,21 @@ function applyBand(age) {
hed.textContent = b.hed;
renderPills(b.id);
renderGrid(b);
+ // Voice greeting on band CHANGE (skip on initial render + don't spam every slider tick).
+ if (_lastSpokenBand !== b.id) {
+ _lastSpokenBand = b.id;
+ const greetings = {
+ toddler: 'Hi friend! Tap the face!',
+ kid: `Hi! You're in kid mode. Click a card to learn more.`,
+ tween: `Tween view. Open any item for details.`,
+ teen: `Teen mode. Dense and dark. Click open for the full record.`,
+ adult: `Adult dashboard. Click open for full details and actions.`,
+ mature: `Mature view. Calmer pacing, larger type.`,
+ senior: `Senior view. High contrast and large buttons. Click view details when ready.`,
+ elder: `Elder view. One item at a time. Click done when you've handled it.`
+ };
+ speak(greetings[b.id] || '', b.id);
+ }
}
slider.addEventListener('input', e => applyBand(Number(e.target.value)));
@@ -383,6 +400,66 @@ function renderGrid(b) {
}
}
+// ===== VOICE — per-band TTS via browser Web Speech API =====
+// Each band picks a voice by ordered name preferences, plus pitch + rate, so the same toast
+// sounds different at every age. Falls back to the default voice if none of the preferences
+// are installed on the user's OS.
+const BAND_VOICES = {
+ toddler: { prefs:['Princess','Junior','Kathy','Samantha','Karen'], pitch:1.6, rate:1.05, volume:1 },
+ kid: { prefs:['Karen','Samantha','Tessa','Moira','Veena'], pitch:1.35, rate:1.0, volume:1 },
+ tween: { prefs:['Samantha','Allison','Karen','Tessa'], pitch:1.15, rate:1.05, volume:1 },
+ teen: { prefs:['Daniel','Aaron','Fred','Tom','Alex'], pitch:1.0, rate:1.08, volume:1 },
+ adult: { prefs:['Samantha','Alex','Allison','Karen'], pitch:1.0, rate:1.0, volume:1 },
+ mature: { prefs:['Veena','Moira','Tessa','Samantha','Karen'], pitch:0.98, rate:0.95, volume:1 },
+ senior: { prefs:['Alex','Tom','Daniel','Aaron','Fred'], pitch:0.95, rate:0.88, volume:1 },
+ elder: { prefs:['Bruce','Tom','Fred','Alex'], pitch:0.85, rate:0.78, volume:1 }
+};
+let _voiceCache = null;
+let _voiceEnabled = true;
+let _selectedVoices = {}; // band -> SpeechSynthesisVoice
+
+function loadVoices() {
+ if (!('speechSynthesis' in window)) return [];
+ _voiceCache = window.speechSynthesis.getVoices();
+ // Match each band's preferences against installed voices
+ _selectedVoices = {};
+ for (const [band, cfg] of Object.entries(BAND_VOICES)) {
+ let chosen = null;
+ for (const pref of cfg.prefs) {
+ chosen = _voiceCache.find(v => v.name === pref || v.name.startsWith(pref + ' '));
+ if (chosen) break;
+ }
+ _selectedVoices[band] = chosen || _voiceCache.find(v => v.lang.startsWith('en')) || _voiceCache[0] || null;
+ }
+ return _voiceCache;
+}
+// Voices populate async on first page load.
+if ('speechSynthesis' in window) {
+ loadVoices();
+ window.speechSynthesis.onvoiceschanged = loadVoices;
+}
+
+function speak(text, band) {
+ if (!_voiceEnabled || !('speechSynthesis' in window) || !text) return;
+ // Cancel anything currently speaking so the user isn't piled up
+ window.speechSynthesis.cancel();
+ const cfg = BAND_VOICES[band] || BAND_VOICES.adult;
+ const u = new SpeechSynthesisUtterance(text);
+ if (_selectedVoices[band]) u.voice = _selectedVoices[band];
+ u.pitch = cfg.pitch;
+ u.rate = cfg.rate;
+ u.volume = cfg.volume;
+ window.speechSynthesis.speak(u);
+}
+
+function toggleVoice() {
+ _voiceEnabled = !_voiceEnabled;
+ const btn = document.getElementById('voiceToggle');
+ if (btn) btn.textContent = _voiceEnabled ? '🔊 voice on' : '🔇 voice off';
+ if (!_voiceEnabled) window.speechSynthesis && window.speechSynthesis.cancel();
+ else speak('voice on', bandFor(Number(slider.value)).id);
+}
+
// ===== UI HOOKS — every band's primary action wired here. =====
// In-memory state for the demo (no API writes); persists for the page session.
const _localActions = new Map(); // failureId -> { resolved, dismissed, refined, opened, celebrated }
@@ -392,6 +469,7 @@ function toast(msg, ms = 1800) {
el.textContent = msg;
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), ms);
+ speak(msg, bandFor(Number(slider.value)).id);
}
function openModal(html) {
@@ -463,13 +541,12 @@ function onAction(band, id, btn) {
const f = findFailure(id);
if (!f) { toast('item not found'); return; }
if (band === 'toddler') {
- // celebrate on tap
_localActions.set(id, { ...(_localActions.get(id)||{}), celebrated: true });
openModal(`<div class="toddler-celebrate">🎉</div><div style="text-align:center; font-size:1.3em; font-weight:700; margin-top:14px;">Good job!</div><div class="modal-actions" style="justify-content:center;"><button class="action-btn" onclick="closeModal()">Okay!</button></div>`);
+ speak('Good job!', 'toddler');
return;
}
if (band === 'elder') {
- // confirm-resolve in one step
openModal(`
<h3 style="font-size:1.4em;">Mark as done?</h3>
<div style="margin:14px 0; font-size:1.15em;">${f.suggestedAction || f.processName}</div>
@@ -477,10 +554,21 @@ function onAction(band, id, btn) {
<button class="action-btn" onclick="markResolved('${id}')">Yes, done</button>
<button class="action-btn" style="background:transparent; color:var(--fg); border:2px solid var(--border);" onclick="closeModal()">Not yet</button>
</div>`);
+ speak(`Mark as done? ${f.suggestedAction || f.processName}`, 'elder');
return;
}
- // all other bands → detail modal
+ // all other bands → detail modal + spoken summary
openModal(detailHTML(f, band));
+ // Band-tuned spoken summary
+ const blurbs = {
+ kid: `Looking at ${f.processName}. ${plainLabel(f)}`,
+ tween: `${f.processName}. Severity ${f.severity || 'unset'}.`,
+ teen: `${f.processName}. ${f.severity || '?'} severity. ${f.eventCount} events.`,
+ adult: `${f.processName}. ${f.severity || 'unset'} severity. ${f.suggestedAction || ''}`,
+ mature: `${f.processName}. Severity ${f.severity || 'unset'}. ${f.suggestedAction || ''}`,
+ senior: `${f.processName}. ${f.severity || 'unset'} priority. ${f.suggestedAction || 'No action required.'}`
+ };
+ speak(blurbs[band] || f.processName, band);
}
// Re-render needs to respect _localActions so resolved items show as resolved.
← 788f2be v7 UI hooks: 8 band quick-jump pills, per-band modal/toast h
·
back to Cncp Failures Mockups
·
v7 voice: replace Web Speech with Steve's ElevenLabs cloned 2f518c5 →