Skip to content

Commit 7157aa0

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

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
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
1212

1313
### Fixes
1414

15+
- 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. Thanks @ngutman.
1516
- 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.
1617
- Discord/presence defaults: send an online presence update on ready when no custom presence is configured so bots no longer appear offline by default. Thanks @thewilloftheshadow.
1718
- Discord/typing cleanup: stop typing indicators after silent/NO_REPLY runs by marking the run complete before dispatch idle cleanup. 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)