From ac21afea21aeee56e73ab769eba5c709ce74a62c Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 14:45:56 +0000 Subject: [PATCH] Rudder's robot voice keeps a sentence whole across a breath, and splits only real run-ons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First live use: one sentence came out as two. - A pause is not a sentence end: the engine finalises after 0.8 s of silence, a breath mid-sentence included, and the robot spoke that fragment alone. A final that doesn't end in . ! or ? is now held and joined to what's said next; spoken as it is after 2.5 s of silence or on Stop. - The comma break for run-ons now needs ≥20 words waiting (was 14) and keeps ≥10 in the piece, so ordinary long sentences are no longer cut at their comma. - Checked by running the page's own functions on scripted streams: a sentence paused mid-way → one spoken sentence; a 30-word run-on → two clause-sized pieces; a 16-word sentence with a comma → not split; an unfinished thought then Stop → still spoken. --- plugin-designs/rudder-voice/rudder-voice.vue | 34 +++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/plugin-designs/rudder-voice/rudder-voice.vue b/plugin-designs/rudder-voice/rudder-voice.vue index 1cf8e3f61..50fd3281c 100644 --- a/plugin-designs/rudder-voice/rudder-voice.vue +++ b/plugin-designs/rudder-voice/rudder-voice.vue @@ -146,6 +146,8 @@ const SENTENCE_END = /[.!?]["')\]]?$/ // 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. +// ★ OVER-SPLITTING (measured 2026-09-28, first live use): at ≥14 words the comma rule also cut ordinary +// long sentences in two. Raised to ≥20 words waiting, keeping ≥10 in the piece spoken. const CLAUSE_END = /[,;:]$/ function robotOnPartial(full) { if (!robotOn.value) return @@ -154,19 +156,42 @@ function robotOnPartial(full) { 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 >= 20) { + for (let i = robotSpoken + 9; 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(' ')) + speak(takeHeld() + w.slice(robotSpoken, end + 1).join(' ')) robotSpoken = end + 1 } } +// ★ A PAUSE IS NOT A SENTENCE END. The engine finalises after 0.8 s of silence — a breath in the middle of a +// sentence included — and the robot used to speak that fragment on its own, so one sentence came out as two. +// A final that doesn't end in . ! or ? is HELD and joined to whatever is said next; if nothing follows within +// 2.5 s (or recording stops) it is spoken as it is. +let robotHeld = '' +let robotHoldTimer = null +function takeHeld() { + clearTimeout(robotHoldTimer) + const h = robotHeld + robotHeld = '' + return h ? h + ' ' : '' +} +function speakHeld() { + const h = takeHeld().trim() + if (h && robotOn.value) speak(h) +} function robotOnFinal(text) { const rest = wordsOf(text).slice(robotSpoken) robotSpoken = 0 - if (robotOn.value && rest.length) speak(rest.join(' ')) + if (!robotOn.value || !rest.length) return + if (SENTENCE_END.test(rest[rest.length - 1])) { + speak(takeHeld() + rest.join(' ')) + } else { + robotHeld = (robotHeld ? robotHeld + ' ' : '') + rest.join(' ') + clearTimeout(robotHoldTimer) + robotHoldTimer = setTimeout(speakHeld, 2500) + } } // A light clean-up of what the ROBOT says (the box keeps exactly what was said): drop filler sounds, collapse @@ -607,6 +632,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 + speakHeld() clearTimeout(flushTimer) const finals = pendingFinals.splice(0) const p = pendingPartial