mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
wasm slop tts
This commit is contained in:
@@ -43,6 +43,11 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"entityId": "laptop",
|
"entityId": "laptop",
|
||||||
"label": "Broken Laptop"
|
"label": "Broken Laptop"
|
||||||
|
},
|
||||||
|
"o006": {
|
||||||
|
"type": "object",
|
||||||
|
"entityId": "radio",
|
||||||
|
"label": "S. I. M. card properly"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -12,7 +12,7 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Roomba Rover" />
|
<meta name="apple-mobile-web-app-title" content="Roomba Rover" />
|
||||||
<title>Roomba Rover</title>
|
<title>Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-suSXs4PI.js"></script>
|
<script type="module" crossorigin src="/assets/index-DXvz_bZm.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BVKa8Zph.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BVKa8Zph.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Generated
+1028
-2
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"kokoro-js": "^1.2.1",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
// 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();
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// Purpose: Speaks server-provided scan labels on the scanner computer.
|
// 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.
|
// 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 { useEffect, useRef } from 'react';
|
||||||
|
import { speakWithKokoro, stopKokoroSpeech } from './scannerTts/kokoroBrowserTts.js';
|
||||||
|
|
||||||
const SPEECH_START_TIMEOUT_MS = 1500;
|
const SPEECH_START_TIMEOUT_MS = 1500;
|
||||||
const SPEECH_RETRY_DELAY_MS = 700;
|
const SPEECH_RETRY_DELAY_MS = 700;
|
||||||
@@ -53,6 +54,7 @@ export default function useScannerSpeech(scan) {
|
|||||||
return () => {
|
return () => {
|
||||||
window.clearTimeout(retryTimerRef.current);
|
window.clearTimeout(retryTimerRef.current);
|
||||||
window.clearTimeout(startTimerRef.current);
|
window.clearTimeout(startTimerRef.current);
|
||||||
|
stopKokoroSpeech();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -77,8 +79,7 @@ export default function useScannerSpeech(scan) {
|
|||||||
retryTimerRef.current = window.setTimeout(() => speakOnce(), SPEECH_RETRY_DELAY_MS);
|
retryTimerRef.current = window.setTimeout(() => speakOnce(), SPEECH_RETRY_DELAY_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
function speakOnce() {
|
function speakWithBrowserFallback() {
|
||||||
if (cancelled) return;
|
|
||||||
const synth = window.speechSynthesis;
|
const synth = window.speechSynthesis;
|
||||||
if (!synth || typeof window.SpeechSynthesisUtterance !== 'function') {
|
if (!synth || typeof window.SpeechSynthesisUtterance !== 'function') {
|
||||||
retryLater();
|
retryLater();
|
||||||
@@ -119,11 +120,25 @@ export default function useScannerSpeech(scan) {
|
|||||||
}, SPEECH_START_TIMEOUT_MS);
|
}, SPEECH_START_TIMEOUT_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
if (cancelled || spoken) return;
|
||||||
|
speakWithBrowserFallback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
speakOnce();
|
speakOnce();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
clearSpeechTimers();
|
clearSpeechTimers();
|
||||||
|
stopKokoroSpeech();
|
||||||
};
|
};
|
||||||
}, [scan]);
|
}, [scan]);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user