fix(voice): preserve tts backpressure and guard utterance callbacks

This commit is contained in:
Nimrod Gutman
2026-03-03 14:26:40 +02:00
parent eb90e004a9
commit 1d3f1fcaf4
2 changed files with 29 additions and 32 deletions

View File

@@ -2214,36 +2214,24 @@ extension TalkModeManager {
textChars: Int
) -> AsyncThrowingStream<Data, Error>
{
AsyncThrowingStream { continuation in
let relay = Task {
var sawFirstChunk = false
do {
for try await chunk in stream {
if !sawFirstChunk {
sawFirstChunk = true
await MainActor.run { [weak self] in
self?.markLatencyAnchorIfNeeded(
\.firstTTSChunkAt,
stage: "tts.chunk.first",
fields: [
"mode=\(mode)",
"bytes=\(chunk.count)",
"textChars=\(textChars)",
])
}
}
continuation.yield(chunk)
}
continuation.finish()
} catch is CancellationError {
continuation.finish()
} catch {
continuation.finish(throwing: error)
var iterator = stream.makeAsyncIterator()
var sawFirstChunk = false
return AsyncThrowingStream {
guard let chunk = try await iterator.next() else { return nil }
if !sawFirstChunk {
sawFirstChunk = true
await MainActor.run { [weak self] in
self?.markLatencyAnchorIfNeeded(
\.firstTTSChunkAt,
stage: "tts.chunk.first",
fields: [
"mode=\(mode)",
"bytes=\(chunk.count)",
"textChars=\(textChars)",
])
}
}
continuation.onTermination = { _ in
relay.cancel()
}
return chunk
}
}

View File

@@ -83,8 +83,13 @@ public final class TalkSystemSpeechSynthesizer: NSObject {
}
}
private func handleFinish(error: Error?) {
guard self.currentUtterance != nil else { return }
private func matchesCurrentUtterance(_ utteranceID: ObjectIdentifier) -> Bool {
guard let currentUtterance = self.currentUtterance else { return false }
return ObjectIdentifier(currentUtterance) == utteranceID
}
private func handleFinish(utteranceID: ObjectIdentifier, error: Error?) {
guard self.matchesCurrentUtterance(utteranceID) else { return }
self.watchdog?.cancel()
self.watchdog = nil
self.finishCurrent(with: error)
@@ -108,7 +113,9 @@ extension TalkSystemSpeechSynthesizer: AVSpeechSynthesizerDelegate {
_ synthesizer: AVSpeechSynthesizer,
didStart utterance: AVSpeechUtterance)
{
let utteranceID = ObjectIdentifier(utterance)
Task { @MainActor in
guard self.matchesCurrentUtterance(utteranceID) else { return }
let callback = self.didStartCallback
self.didStartCallback = nil
callback?()
@@ -119,8 +126,9 @@ extension TalkSystemSpeechSynthesizer: AVSpeechSynthesizerDelegate {
_ synthesizer: AVSpeechSynthesizer,
didFinish utterance: AVSpeechUtterance)
{
let utteranceID = ObjectIdentifier(utterance)
Task { @MainActor in
self.handleFinish(error: nil)
self.handleFinish(utteranceID: utteranceID, error: nil)
}
}
@@ -128,8 +136,9 @@ extension TalkSystemSpeechSynthesizer: AVSpeechSynthesizerDelegate {
_ synthesizer: AVSpeechSynthesizer,
didCancel utterance: AVSpeechUtterance)
{
let utteranceID = ObjectIdentifier(utterance)
Task { @MainActor in
self.handleFinish(error: SpeakError.canceled)
self.handleFinish(utteranceID: utteranceID, error: SpeakError.canceled)
}
}
}