barcode scanner beginnings

This commit is contained in:
legop3
2026-06-09 14:27:48 -04:00
parent 5dca976f97
commit 5f00d8c715
15 changed files with 616 additions and 134 deletions
@@ -0,0 +1,86 @@
// Scanner Speech Hook
// Purpose: Speaks server-provided scan labels on the scanner computer.
// Scope: Keeps browser TTS as a local output device while leaving scan meaning and phrase selection on the server.
import { useEffect, useRef } from 'react';
const SPEECH_START_TIMEOUT_MS = 1500;
const SPEECH_RETRY_DELAY_MS = 700;
export default function useScannerSpeech(scan) {
const retryTimerRef = useRef(null);
const startTimerRef = useRef(null);
const spokenScanKeyRef = useRef(null);
useEffect(() => {
return () => {
window.clearTimeout(retryTimerRef.current);
window.clearTimeout(startTimerRef.current);
};
}, []);
useEffect(() => {
const speechText = String(scan?.speechText || '').trim();
const scanKey = scan?.scannedAt ? `${scan.scannedAt}:${speechText}` : '';
if (!speechText || !scanKey || spokenScanKeyRef.current === scanKey) return undefined;
let cancelled = false;
spokenScanKeyRef.current = scanKey;
function clearSpeechTimers() {
window.clearTimeout(retryTimerRef.current);
window.clearTimeout(startTimerRef.current);
retryTimerRef.current = null;
startTimerRef.current = null;
}
function retryLater() {
if (cancelled) return;
clearSpeechTimers();
retryTimerRef.current = window.setTimeout(() => speakOnce(), SPEECH_RETRY_DELAY_MS);
}
function speakOnce() {
if (cancelled) return;
const synth = window.speechSynthesis;
if (!synth || typeof window.SpeechSynthesisUtterance !== 'function') {
retryLater();
return;
}
clearSpeechTimers();
// Cancelling before retrying prevents stale utterances from piling up if a
// browser reports an error or never fires the expected start callback.
synth.cancel();
const utterance = new window.SpeechSynthesisUtterance(speechText);
utterance.rate = 0.92;
utterance.pitch = 1;
utterance.volume = 1;
utterance.onstart = () => {
window.clearTimeout(startTimerRef.current);
startTimerRef.current = null;
};
utterance.onend = () => {
clearSpeechTimers();
};
utterance.onerror = () => {
retryLater();
};
synth.speak(utterance);
// Some browser/audio states fail without surfacing onerror. A small start
// watchdog keeps the required scanner audio from silently dying, while the
// UI remains clean and rover-readable.
startTimerRef.current = window.setTimeout(() => {
synth.cancel();
retryLater();
}, SPEECH_START_TIMEOUT_MS);
}
speakOnce();
return () => {
cancelled = true;
clearSpeechTimers();
};
}, [scan]);
}