Skip to content

Commit d1acf72

Browse files
mbelinkyngutman
andcommitted
fix(ios): start incremental speech at soft boundaries
Co-authored-by: Nimrod Gutman <[email protected]>
1 parent 22e33dd commit d1acf72

File tree

3 files changed

+38
-2
lines changed

3 files changed

+38
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
1515

1616
- Docs/security hardening guidance: document Docker `DOCKER-USER` + UFW policy and add cross-linking from Docker install docs for VPS/public-host setups. (#27613) thanks @dorukardahan.
1717
- iOS/Voice timing safety: guard system speech start/finish callbacks to the active utterance to avoid misattributed start events during rapid stop/restart cycles. (#33304) thanks @mbelinky; original implementation direction by @ngutman.
18+
- iOS/Talk incremental speech pacing: allow long punctuation-free assistant chunks to start speaking at safe whitespace boundaries so voice responses begin sooner instead of waiting for terminal punctuation. (#33305) thanks @mbelinky; original implementation by @ngutman.
1819
- Docs/tool-loop detection config keys: align `docs/tools/loop-detection.md` examples and field names with the current `tools.loopDetection` schema to prevent copy-paste validation failures from outdated keys. (#33182) Thanks @Mylszd.
1920
- Gateway/session agent discovery: include disk-scanned agent IDs in `listConfiguredAgentIds` even when `agents.list` is configured, so disk-only/ACP agent sessions remain visible in gateway session aggregation and listings. (#32831) thanks @Sid-Qin.
2021
- Discord/inbound debouncer: skip bot-own MESSAGE_CREATE events before they reach the debounce queue to avoid self-triggered slowdowns in busy servers. Thanks @thewilloftheshadow.

apps/ios/Sources/Voice/TalkModeManager.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1682,6 +1682,8 @@ final class TalkModeManager: NSObject {
16821682
}
16831683

16841684
private struct IncrementalSpeechBuffer {
1685+
private static let softBoundaryMinChars = 72
1686+
16851687
private(set) var latestText: String = ""
16861688
private(set) var directive: TalkDirective?
16871689
private var spokenOffset: Int = 0
@@ -1774,8 +1776,9 @@ private struct IncrementalSpeechBuffer {
17741776
}
17751777

17761778
if !inCodeBlock {
1777-
buffer.append(chars[idx])
1778-
if Self.isBoundary(chars[idx]) {
1779+
let currentChar = chars[idx]
1780+
buffer.append(currentChar)
1781+
if Self.isBoundary(currentChar) || Self.isSoftBoundary(currentChar, bufferedChars: buffer.count) {
17791782
lastBoundary = idx + 1
17801783
bufferAtBoundary = buffer
17811784
inCodeBlockAtBoundary = inCodeBlock
@@ -1802,6 +1805,10 @@ private struct IncrementalSpeechBuffer {
18021805
private static func isBoundary(_ ch: Character) -> Bool {
18031806
ch == "." || ch == "!" || ch == "?" || ch == "\n"
18041807
}
1808+
1809+
private static func isSoftBoundary(_ ch: Character, bufferedChars: Int) -> Bool {
1810+
bufferedChars >= Self.softBoundaryMinChars && ch.isWhitespace
1811+
}
18051812
}
18061813

18071814
extension TalkModeManager {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Testing
2+
@testable import OpenClaw
3+
4+
@MainActor
5+
@Suite struct TalkModeIncrementalSpeechBufferTests {
6+
@Test func emitsSoftBoundaryBeforeTerminalPunctuation() {
7+
let manager = TalkModeManager(allowSimulatorCapture: true)
8+
manager._test_incrementalReset()
9+
10+
let partial =
11+
"We start speaking earlier by splitting this long stream chunk at a whitespace boundary before punctuation arrives"
12+
let segments = manager._test_incrementalIngest(partial, isFinal: false)
13+
14+
#expect(segments.count == 1)
15+
#expect(segments[0].count >= 72)
16+
#expect(segments[0].count < partial.count)
17+
}
18+
19+
@Test func keepsShortChunkBufferedWithoutPunctuation() {
20+
let manager = TalkModeManager(allowSimulatorCapture: true)
21+
manager._test_incrementalReset()
22+
23+
let short = "short chunk without punctuation"
24+
let segments = manager._test_incrementalIngest(short, isFinal: false)
25+
26+
#expect(segments.isEmpty)
27+
}
28+
}

0 commit comments

Comments
 (0)