mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 21:13:58 +00:00
* test(gateway): shift advertised-method window for tts.speak * feat(ios): add Listen action speaking assistant messages via gateway TTS * docs(ios): document the chat Listen action * style(ios): keep outbox label doc comment attached and give speech chip its own * fix(ios): harden chat Listen playback * docs(changelog): note iOS chat Listen * fix(ios): unwrap speech MIME before classification * docs(changelog): keep Listen note app-scoped
59 lines
2.0 KiB
Swift
59 lines
2.0 KiB
Swift
import Foundation
|
|
import OpenClawChatUI
|
|
import OpenClawKit
|
|
import OpenClawProtocol
|
|
|
|
/// Backs the chat "Listen" action with the gateway `tts.speak` method, which
|
|
/// renders text with the operator's configured TTS provider chain.
|
|
enum ChatMessageSpeechClient {
|
|
typealias Request = (_ method: String, _ paramsJSON: String?, _ timeoutSeconds: Int) async throws -> Data
|
|
private static let requestTimeoutSeconds = 60
|
|
|
|
static func synthesize(
|
|
text: String,
|
|
gateway: GatewayNodeSession) async throws -> OpenClawChatSpeechClip
|
|
{
|
|
try await self.synthesize(text: text) { method, paramsJSON, timeoutSeconds in
|
|
try await gateway.request(
|
|
method: method,
|
|
paramsJSON: paramsJSON,
|
|
timeoutSeconds: timeoutSeconds)
|
|
}
|
|
}
|
|
|
|
static func synthesize(
|
|
text: String,
|
|
request: Request) async throws -> OpenClawChatSpeechClip
|
|
{
|
|
let params = TtsSpeakParams(text: text)
|
|
let paramsData = try JSONEncoder().encode(params)
|
|
guard let paramsJSON = String(data: paramsData, encoding: .utf8) else {
|
|
throw ChatMessageSpeechError.invalidRequest
|
|
}
|
|
let responseData = try await request("tts.speak", paramsJSON, Self.requestTimeoutSeconds)
|
|
let response = try JSONDecoder().decode(TtsSpeakResult.self, from: responseData)
|
|
guard let audioData = Data(base64Encoded: response.audiobase64), !audioData.isEmpty else {
|
|
throw ChatMessageSpeechError.emptyAudio
|
|
}
|
|
return OpenClawChatSpeechClip(
|
|
data: audioData,
|
|
outputFormat: response.outputformat,
|
|
mimeType: response.mimetype,
|
|
fileExtension: response.fileextension)
|
|
}
|
|
}
|
|
|
|
private enum ChatMessageSpeechError: LocalizedError {
|
|
case invalidRequest
|
|
case emptyAudio
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .invalidRequest:
|
|
"Failed to encode tts.speak request"
|
|
case .emptyAudio:
|
|
"Gateway tts.speak returned empty audio"
|
|
}
|
|
}
|
|
}
|