// Scanner Voice Setup // Purpose: Lets the scanner computer pick one exact browser TTS voice and store it locally. // Scope: Keeps voice selection out of the rover-facing scanner screen while avoiding unreliable voice-name guessing. import { useCallback, useEffect, useState } from 'react'; import { SCANNER_VOICE_STORAGE_KEY } from './useScannerSpeech.js'; function describeVoice(voice) { const parts = [voice?.name || 'unknown voice']; if (voice?.lang) parts.push(voice.lang); if (voice?.default) parts.push('default'); return parts.join(' ยท '); } function speakVoiceSample(voice) { const synth = window.speechSynthesis; if (!synth || typeof window.SpeechSynthesisUtterance !== 'function') return; // Setup speech is intentionally direct and uncached. The operator is testing // individual installed voices, so each button should cancel the previous // sample and immediately speak with the candidate voice. synth.cancel(); const utterance = new window.SpeechSynthesisUtterance('object 1'); utterance.voice = voice; utterance.lang = voice?.lang || 'en-US'; utterance.rate = 1; utterance.pitch = 1; utterance.volume = 1; synth.speak(utterance); } function readBrowserVoices() { const synth = window.speechSynthesis; return typeof synth?.getVoices === 'function' ? synth.getVoices() : []; } export default function ScannerVoiceSetup() { const [voices, setVoices] = useState(() => readBrowserVoices()); const [selectedVoiceName, setSelectedVoiceName] = useState(() => String(window.localStorage.getItem(SCANNER_VOICE_STORAGE_KEY) || '').trim(), ); const refreshVoices = useCallback(() => { const nextVoices = readBrowserVoices(); setVoices(nextVoices); // Logging the exact voice list is useful on Linux because Firefox exposes // whatever the system speech stack provides, and those names are the only // reliable identifiers we can save for later scanner sessions. console.info('[scanner voice setup] available voices', nextVoices.map(describeVoice)); }, []); useEffect(() => { const refreshTimer = window.setTimeout(refreshVoices, 0); const synth = window.speechSynthesis; if (!synth) { return () => { window.clearTimeout(refreshTimer); }; } synth.addEventListener?.('voiceschanged', refreshVoices); synth.onvoiceschanged = refreshVoices; return () => { window.clearTimeout(refreshTimer); synth.removeEventListener?.('voiceschanged', refreshVoices); if (synth.onvoiceschanged === refreshVoices) { synth.onvoiceschanged = null; } }; }, [refreshVoices]); const saveVoice = useCallback((voice) => { const voiceName = String(voice?.name || '').trim(); if (!voiceName) return; window.localStorage.setItem(SCANNER_VOICE_STORAGE_KEY, voiceName); setSelectedVoiceName(voiceName); speakVoiceSample(voice); }, []); const clearVoice = useCallback(() => { window.localStorage.removeItem(SCANNER_VOICE_STORAGE_KEY); setSelectedVoiceName(''); const synth = window.speechSynthesis; if (synth) synth.cancel(); }, []); return (

scanner voice

Pick the exact voice this scanner computer should use.

Selected: {selectedVoiceName || 'automatic fallback'}

back to scanner
{voices.length ? voices.map((voice) => { const selected = String(voice?.name || '') === selectedVoiceName; return (
{describeVoice(voice)}
); }) : (
no browser voices loaded
)}
); }