mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
adding barcodesses. piper slop tts
This commit is contained in:
@@ -48,6 +48,11 @@
|
||||
"type": "object",
|
||||
"entityId": "radio",
|
||||
"label": "S. I. M. card properly"
|
||||
},
|
||||
"o007": {
|
||||
"type": "object",
|
||||
"entityId": "printer",
|
||||
"label": "Medical thermal printer"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -12,7 +12,7 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Roomba Rover" />
|
||||
<title>Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-DXvz_bZm.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-h2u2QzDn.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BVKa8Zph.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Generated
+50
-909
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -10,7 +10,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"kokoro-js": "^1.2.1",
|
||||
"@mintplex-labs/piper-tts-web": "^1.0.4",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-icons": "^5.5.0",
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
// Kokoro Browser TTS Adapter
|
||||
// Purpose: Runs scanner speech through Kokoro.js locally in the browser when available.
|
||||
// Scope: Keeps the heavy model dependency behind a dynamic import so non-scanner pages do not load it.
|
||||
|
||||
const KOKORO_MODEL_ID = 'onnx-community/Kokoro-82M-v1.0-ONNX';
|
||||
const KOKORO_OPTIONS = {
|
||||
// q8 is the practical first pass for a scanner station: much smaller than
|
||||
// fp32/fp16, still good enough for short object labels, and it runs on plain
|
||||
// WASM so Firefox does not need WebGPU support.
|
||||
dtype: 'q8',
|
||||
device: 'wasm',
|
||||
};
|
||||
const KOKORO_VOICE = 'af_bella';
|
||||
const KOKORO_SPEED = 1;
|
||||
|
||||
let kokoroModelPromise = null;
|
||||
let currentAudio = null;
|
||||
let currentObjectUrl = null;
|
||||
|
||||
function stopCurrentAudio() {
|
||||
if (currentAudio) {
|
||||
// Scanner scans can arrive close together. Stop the previous generated clip
|
||||
// before starting the next one so the spoken label always matches the most
|
||||
// recent server scan state.
|
||||
currentAudio.pause();
|
||||
currentAudio.removeAttribute('src');
|
||||
currentAudio.load();
|
||||
currentAudio = null;
|
||||
}
|
||||
if (currentObjectUrl) {
|
||||
URL.revokeObjectURL(currentObjectUrl);
|
||||
currentObjectUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadKokoroModel() {
|
||||
if (!kokoroModelPromise) {
|
||||
kokoroModelPromise = import('kokoro-js')
|
||||
.then(async ({ KokoroTTS }) => {
|
||||
console.info('[scanner tts] loading Kokoro model');
|
||||
const model = await KokoroTTS.from_pretrained(KOKORO_MODEL_ID, {
|
||||
...KOKORO_OPTIONS,
|
||||
progress_callback: (progress) => {
|
||||
// The normal scanner page stays clean, but console progress is very
|
||||
// useful when first-load model downloads are slow on the scanner PC.
|
||||
if (progress?.status) {
|
||||
console.info('[scanner tts] Kokoro load', progress);
|
||||
}
|
||||
},
|
||||
});
|
||||
console.info('[scanner tts] Kokoro model ready');
|
||||
return model;
|
||||
})
|
||||
.catch((err) => {
|
||||
// Reset the promise after a failure so a later scan can retry. This is
|
||||
// important for first-load network hiccups while still letting the hook
|
||||
// fall back to browser speech immediately.
|
||||
kokoroModelPromise = null;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
return kokoroModelPromise;
|
||||
}
|
||||
|
||||
function playBlob(blob) {
|
||||
stopCurrentAudio();
|
||||
currentObjectUrl = URL.createObjectURL(blob);
|
||||
currentAudio = new Audio(currentObjectUrl);
|
||||
currentAudio.volume = 1;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const audio = currentAudio;
|
||||
audio.onended = () => {
|
||||
if (currentAudio === audio) {
|
||||
stopCurrentAudio();
|
||||
}
|
||||
resolve(true);
|
||||
};
|
||||
audio.onerror = () => {
|
||||
if (currentAudio === audio) {
|
||||
stopCurrentAudio();
|
||||
}
|
||||
reject(new Error('Kokoro audio playback failed'));
|
||||
};
|
||||
audio.play().catch((err) => {
|
||||
if (currentAudio === audio) {
|
||||
stopCurrentAudio();
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function speakWithKokoro(text) {
|
||||
const cleanText = String(text || '').replace(/\s+/g, ' ').trim();
|
||||
if (!cleanText) return false;
|
||||
|
||||
try {
|
||||
const model = await loadKokoroModel();
|
||||
const audio = await model.generate(cleanText, {
|
||||
voice: KOKORO_VOICE,
|
||||
speed: KOKORO_SPEED,
|
||||
});
|
||||
await playBlob(audio.toBlob());
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn('[scanner tts] Kokoro unavailable; falling back to browser speech', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function stopKokoroSpeech() {
|
||||
stopCurrentAudio();
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Piper Browser TTS Adapter
|
||||
// Purpose: Runs scanner speech through Piper locally in the browser when available.
|
||||
// Scope: Keeps the neural TTS package behind a dynamic import so non-scanner pages do not load it.
|
||||
|
||||
const PIPER_VOICE_ID = 'en_US-hfc_female-medium';
|
||||
const PIPER_LIVE_TIMEOUT_MS = 900;
|
||||
|
||||
let piperModulePromise = null;
|
||||
let currentAudio = null;
|
||||
let currentObjectUrl = null;
|
||||
let activeRequestId = 0;
|
||||
|
||||
function stopCurrentAudio() {
|
||||
if (currentAudio) {
|
||||
// Scanner labels should never overlap. Stop the previous clip before the
|
||||
// next scan speaks so audio always matches the current large display text.
|
||||
currentAudio.pause();
|
||||
currentAudio.removeAttribute('src');
|
||||
currentAudio.load();
|
||||
currentAudio = null;
|
||||
}
|
||||
if (currentObjectUrl) {
|
||||
URL.revokeObjectURL(currentObjectUrl);
|
||||
currentObjectUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPiperModule() {
|
||||
if (!piperModulePromise) {
|
||||
piperModulePromise = import('@mintplex-labs/piper-tts-web')
|
||||
.then((module) => {
|
||||
console.info('[scanner tts] Piper module ready');
|
||||
return module;
|
||||
})
|
||||
.catch((err) => {
|
||||
// Reset after failures so later scans can try again after a transient
|
||||
// model/CDN/cache issue. The hook falls back to browser speech for the
|
||||
// current scan rather than blocking the scanner interaction.
|
||||
piperModulePromise = null;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
return piperModulePromise;
|
||||
}
|
||||
|
||||
function playBlob(blob) {
|
||||
stopCurrentAudio();
|
||||
currentObjectUrl = URL.createObjectURL(blob);
|
||||
currentAudio = new Audio(currentObjectUrl);
|
||||
currentAudio.volume = 1;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const audio = currentAudio;
|
||||
audio.onended = () => {
|
||||
if (currentAudio === audio) {
|
||||
stopCurrentAudio();
|
||||
}
|
||||
resolve(true);
|
||||
};
|
||||
audio.onerror = () => {
|
||||
if (currentAudio === audio) {
|
||||
stopCurrentAudio();
|
||||
}
|
||||
reject(new Error('Piper audio playback failed'));
|
||||
};
|
||||
audio.play().catch((err) => {
|
||||
if (currentAudio === audio) {
|
||||
stopCurrentAudio();
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function speakWithPiper(text) {
|
||||
const cleanText = String(text || '').replace(/\s+/g, ' ').trim();
|
||||
if (!cleanText) return false;
|
||||
const requestId = activeRequestId + 1;
|
||||
activeRequestId = requestId;
|
||||
|
||||
try {
|
||||
const wavBlob = await Promise.race([
|
||||
loadPiperModule().then((piper) =>
|
||||
piper.predict(
|
||||
{
|
||||
text: cleanText,
|
||||
voiceId: PIPER_VOICE_ID,
|
||||
},
|
||||
(progress) => {
|
||||
// Piper stores downloaded models in the browser origin-private file
|
||||
// system. Console progress is enough for setup/debugging while the
|
||||
// rover-facing scanner screen stays uncluttered.
|
||||
if (progress?.url) {
|
||||
console.info('[scanner tts] Piper load', progress);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
new Promise((resolve) => {
|
||||
window.setTimeout(() => resolve(null), PIPER_LIVE_TIMEOUT_MS);
|
||||
}),
|
||||
]);
|
||||
if (!wavBlob || requestId !== activeRequestId) {
|
||||
return false;
|
||||
}
|
||||
await playBlob(wavBlob);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn('[scanner tts] Piper unavailable; falling back to browser speech', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function stopPiperSpeech() {
|
||||
activeRequestId += 1;
|
||||
stopCurrentAudio();
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// 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';
|
||||
import { speakWithKokoro, stopKokoroSpeech } from './scannerTts/kokoroBrowserTts.js';
|
||||
import { speakWithPiper, stopPiperSpeech } from './scannerTts/piperBrowserTts.js';
|
||||
|
||||
const SPEECH_START_TIMEOUT_MS = 1500;
|
||||
const SPEECH_RETRY_DELAY_MS = 700;
|
||||
@@ -54,7 +54,7 @@ export default function useScannerSpeech(scan) {
|
||||
return () => {
|
||||
window.clearTimeout(retryTimerRef.current);
|
||||
window.clearTimeout(startTimerRef.current);
|
||||
stopKokoroSpeech();
|
||||
stopPiperSpeech();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -123,11 +123,12 @@ export default function useScannerSpeech(scan) {
|
||||
function speakOnce() {
|
||||
if (cancelled) return;
|
||||
clearSpeechTimers();
|
||||
stopKokoroSpeech();
|
||||
// Kokoro is attempted first because it gives the scanner page a consistent
|
||||
// local model voice instead of relying on the OS/browser Web Speech voice
|
||||
// list. The old browser TTS path remains the reliability fallback.
|
||||
speakWithKokoro(speechText).then((spoken) => {
|
||||
stopPiperSpeech();
|
||||
// Piper is attempted first because it is lighter than Kokoro while still
|
||||
// giving the scanner page a consistent local model voice. The old browser
|
||||
// TTS path remains the reliability fallback if Piper is still loading or
|
||||
// fails on the scanner computer.
|
||||
speakWithPiper(speechText).then((spoken) => {
|
||||
if (cancelled || spoken) return;
|
||||
speakWithBrowserFallback();
|
||||
});
|
||||
@@ -138,7 +139,7 @@ export default function useScannerSpeech(scan) {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearSpeechTimers();
|
||||
stopKokoroSpeech();
|
||||
stopPiperSpeech();
|
||||
};
|
||||
}, [scan]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user