Rudder's robot voice keeps up with non-stop talk, skips fillers, and defaults to Male voice 5
- Speaks each sentence as soon as it's complete in the stream, not only after a pause. MEASURED: with no
pause between sentences the model joins them with a comma, not a period, so there may be no sentence end
for 20+ s; a long stretch (≥14 words) now also breaks at the last comma (≥8 words), and ≥35 words with no
punctuation speaks the first 25. Short fragments (<3 words) ride along with the next chunk. On 27 s of
speech with no pause the robot spoke at 8.4 s, 16.5 s and 24.1 s instead of silent until 21.7 s; every
word is spoken exactly once, in order.
- Cleans what the ROBOT says (the box keeps exactly what was said): drops um/uh/erm/hmm/ah with their
commas, collapses stuttered repeats ("the the"), tidies spacing, capitalises. Text rules only.
- Default voice: Male voice 5 (speaker 375) — the clearest by ear; the Robot voice itself stays off until
ticked.
This commit is contained in:
parent
45c2ce9bcc
commit
971162a4b6
1 changed files with 57 additions and 3 deletions
|
|
@ -124,13 +124,65 @@ async function loadRobotVoices() {
|
|||
for (const v of synth.getVoices().filter((x) => x.localService).slice(0, 10)) opts.push({ value: 'system:' + v.voiceURI, label: 'System voice · ' + v.name })
|
||||
}
|
||||
robotOptions.value = opts
|
||||
if (!opts.some((o) => o.value === robotVoice.value)) robotVoice.value = opts[0] ? opts[0].value : ''
|
||||
if (!opts.some((o) => o.value === robotVoice.value)) {
|
||||
// Default: Male voice 5 (speaker 375) — chosen by ear as the clearest; 1–4 sounded muffled.
|
||||
const preferred = opts.find((o) => /:375$/.test(o.value))
|
||||
robotVoice.value = preferred ? preferred.value : opts[0] ? opts[0].value : ''
|
||||
}
|
||||
}
|
||||
|
||||
const sayQueue = []
|
||||
let saying = false
|
||||
// ─── What the robot says: whole sentences, as soon as they're complete, cleaned up ───────────────
|
||||
// The engine only sends a FINAL after a pause, so during a minute of continuous talk the robot fell silent
|
||||
// and then read everything at once. The model already punctuates as it streams, so a sentence is ready the
|
||||
// moment a word ending in . ! or ? is followed by more words: speak it then, without waiting for a pause.
|
||||
// Fragments under 3 words ("Okay.") wait and ride along with the next sentence. robotSpoken counts the words
|
||||
// of the current utterance already handed to the robot; the final speaks only what's left.
|
||||
let robotSpoken = 0
|
||||
const SENTENCE_END = /[.!?]["')\]]?$/
|
||||
// ★ RUN-ONS (measured 2026-09-28): with no pause between sentences the model joins them with a COMMA, not
|
||||
// a period ("…immediately afterwards, going along…"), so during non-stop ideation there may be no sentence
|
||||
// end for 20+ seconds. So a long pending stretch also breaks at a clause: ≥14 words waiting → speak up to the
|
||||
// last comma (keeping ≥8 words); ≥35 words with no comma at all → speak the first 25, so the robot never
|
||||
// falls silent for long.
|
||||
const CLAUSE_END = /[,;:]$/
|
||||
function robotOnPartial(full) {
|
||||
if (!robotOn.value) return
|
||||
const w = wordsOf(full)
|
||||
const done = w.length - 1 // the last word may be half-streamed: only words before it are settled
|
||||
const pending = done - robotSpoken
|
||||
let end = -1
|
||||
for (let i = robotSpoken; i < done; i++) if (SENTENCE_END.test(w[i])) end = i
|
||||
if (end < 0 && pending >= 14) {
|
||||
for (let i = robotSpoken + 7; i < done; i++) if (CLAUSE_END.test(w[i])) end = i
|
||||
if (end < 0 && pending >= 35) end = robotSpoken + 24
|
||||
}
|
||||
if (end >= robotSpoken && end - robotSpoken + 1 >= 3) {
|
||||
speak(w.slice(robotSpoken, end + 1).join(' '))
|
||||
robotSpoken = end + 1
|
||||
}
|
||||
}
|
||||
function robotOnFinal(text) {
|
||||
const rest = wordsOf(text).slice(robotSpoken)
|
||||
robotSpoken = 0
|
||||
if (robotOn.value && rest.length) speak(rest.join(' '))
|
||||
}
|
||||
|
||||
// A light clean-up of what the ROBOT says (the box keeps exactly what was said): drop filler sounds, collapse
|
||||
// stuttered repeats ("the the"), tidy spacing, capitalise. Pure text rules, no model, no extra compute.
|
||||
// A filler takes its commas with it (", um," → " "), but never a full stop that ends a sentence.
|
||||
const FILLERS = /(?:,\s*)?\b(?:u+m+|u+h+m*|e+r+m+|h+m+|a+h+)\b(?:\s*,)?/gi
|
||||
function cleanForSpeech(t) {
|
||||
let s = (t || '').replace(FILLERS, ' ')
|
||||
s = s.replace(/\b(\w+)(?:[,\s]+\1\b)+/gi, '$1')
|
||||
s = s.replace(/\s+([,.!?;:])/g, '$1').replace(/([,.!?;:])(?=[^\s\d])/g, '$1 ').replace(/\s{2,}/g, ' ').trim()
|
||||
s = s.replace(/^[,.;:!?\s]+/, '')
|
||||
return s ? s[0].toUpperCase() + s.slice(1) : ''
|
||||
}
|
||||
|
||||
function speak(text) {
|
||||
const t = (text || '').trim()
|
||||
const t = cleanForSpeech(text)
|
||||
if (!t || !robotVoice.value) return
|
||||
sayQueue.push(t)
|
||||
if (!saying) drainSpeech()
|
||||
|
|
@ -440,11 +492,12 @@ function onEvent(data) {
|
|||
case 'partial':
|
||||
partial.value += e.text
|
||||
onPartial(partial.value)
|
||||
robotOnPartial(partial.value)
|
||||
break
|
||||
case 'final':
|
||||
partial.value = ''
|
||||
onFinal(e.text)
|
||||
if (robotOn.value) speak(e.text)
|
||||
robotOnFinal(e.text)
|
||||
break
|
||||
case 'recovering':
|
||||
say(e.message, 'warn')
|
||||
|
|
@ -553,6 +606,7 @@ function closeStream() {
|
|||
|
||||
// Recording stopped mid-sentence: the streamed words are already in the box, so just let go of them.
|
||||
function flushPartial() {
|
||||
robotSpoken = 0
|
||||
clearTimeout(flushTimer)
|
||||
const finals = pendingFinals.splice(0)
|
||||
const p = pendingPartial
|
||||
|
|
|
|||
Loading…
Reference in a new issue