diff --git a/extensions/inworld/tts.test.ts b/extensions/inworld/tts.test.ts index 87cd8704b1d..6c61d66aa2c 100644 --- a/extensions/inworld/tts.test.ts +++ b/extensions/inworld/tts.test.ts @@ -237,6 +237,15 @@ describe("inworldTTS", () => { ); }); + it("rejects malformed base64 audio chunks", async () => { + const body = JSON.stringify({ result: { audioContent: "not-base64!" } }); + queueGuardedResponse(new Response(body, { status: 200 })); + + await expect(inworldTTS({ text: "test", apiKey: "fixture-api-key" })).rejects.toThrow( + "Inworld TTS returned malformed base64 audio data", + ); + }); + it("throws on HTTP errors with response body", async () => { queueGuardedResponse(new Response("bad request body", { status: 400 })); diff --git a/extensions/inworld/tts.ts b/extensions/inworld/tts.ts index a21e067131a..25571dfa30d 100644 --- a/extensions/inworld/tts.ts +++ b/extensions/inworld/tts.ts @@ -1,5 +1,5 @@ // Inworld plugin module implements tts behavior. -import { MAX_AUDIO_BYTES } from "openclaw/plugin-sdk/media-runtime"; +import { canonicalizeBase64, MAX_AUDIO_BYTES } from "openclaw/plugin-sdk/media-runtime"; import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime"; import type { SpeechVoiceOption } from "openclaw/plugin-sdk/speech-core"; import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime"; @@ -186,7 +186,11 @@ export async function inworldTTS(params: { } if (parsed.result?.audioContent) { - const chunk = Buffer.from(parsed.result.audioContent, "base64"); + const canonicalAudio = canonicalizeBase64(parsed.result.audioContent); + if (!canonicalAudio) { + throw new Error("Inworld TTS returned malformed base64 audio data"); + } + const chunk = Buffer.from(canonicalAudio, "base64"); const nextDecodedAudioBytes = decodedAudioBytes + chunk.length; if (nextDecodedAudioBytes > MAX_AUDIO_BYTES) { throw new Error(