From 2afd65741cdaa4808f43b11a0947a8f1fe6fe257 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Fri, 20 Mar 2026 11:07:13 +0530 Subject: [PATCH] fix: preserve talk provider and speaking state --- .../ai/openclaw/app/voice/TalkModeManager.kt | 2 +- src/gateway/server-methods/talk.ts | 2 - src/gateway/server.talk-config.test.ts | 52 +++++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt b/apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt index d4433d72a9c..2a82588b46b 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt @@ -748,7 +748,7 @@ class TalkModeManager( private suspend fun playGatewaySpeech(speech: GatewayTalkSpeech, playbackToken: Long) { ensurePlaybackActive(playbackToken) - stopSpeaking(resetInterrupt = false) + cleanupPlayer() ensurePlaybackActive(playbackToken) val audioBytes = diff --git a/src/gateway/server-methods/talk.ts b/src/gateway/server-methods/talk.ts index acbede0b33d..3930dc4c4ca 100644 --- a/src/gateway/server-methods/talk.ts +++ b/src/gateway/server-methods/talk.ts @@ -171,8 +171,6 @@ function buildTalkTtsConfig( ...(proxy == null ? {} : { proxy }), ...(timeoutMs == null ? {} : { timeoutMs }), }; - } else { - return { error: `talk.speak unavailable: unsupported talk provider '${resolved.provider}'` }; } return { diff --git a/src/gateway/server.talk-config.test.ts b/src/gateway/server.talk-config.test.ts index 6433445795f..1dccbfab5c6 100644 --- a/src/gateway/server.talk-config.test.ts +++ b/src/gateway/server.talk-config.test.ts @@ -6,6 +6,8 @@ import { publicKeyRawBase64UrlFromPem, signDevicePayload, } from "../infra/device-identity.js"; +import { createEmptyPluginRegistry } from "../plugins/registry-empty.js"; +import { getActivePluginRegistry, setActivePluginRegistry } from "../plugins/runtime.js"; import { withEnvAsync } from "../test-utils/env.js"; import { buildDeviceAuthPayload } from "./device-auth.js"; import { validateTalkConfigResult } from "./protocol/index.js"; @@ -348,4 +350,54 @@ describe("gateway talk.config", () => { globalThis.fetch = originalFetch; } }); + + it("allows extension speech providers through talk.speak", async () => { + const { writeConfigFile } = await import("../config/config.js"); + await writeConfigFile({ + talk: { + provider: "acme", + providers: { + acme: { + voiceId: "plugin-voice", + }, + }, + }, + }); + + const previousRegistry = getActivePluginRegistry() ?? createEmptyPluginRegistry(); + setActivePluginRegistry({ + ...createEmptyPluginRegistry(), + speechProviders: [ + { + pluginId: "acme-plugin", + source: "test", + provider: { + id: "acme", + label: "Acme Speech", + isConfigured: () => true, + synthesize: async () => ({ + audioBuffer: Buffer.from([7, 8, 9]), + outputFormat: "mp3", + fileExtension: ".mp3", + voiceCompatible: false, + }), + }, + }, + ], + }); + + try { + await withServer(async (ws) => { + await connectOperator(ws, ["operator.read", "operator.write"]); + const res = await fetchTalkSpeak(ws, { + text: "Hello from plugin talk mode.", + }); + expect(res.ok).toBe(true); + expect(res.payload?.provider).toBe("acme"); + expect(res.payload?.audioBase64).toBe(Buffer.from([7, 8, 9]).toString("base64")); + }); + } finally { + setActivePluginRegistry(previousRegistry); + } + }); });