mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 19:07:26 +00:00
fix(voice): preserve tts backpressure and guard utterance callbacks
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user