diff --git a/src/__tests__/azure-mstts-namespace.test.ts b/src/__tests__/azure-mstts-namespace.test.ts index e49a19d..d5d291e 100644 --- a/src/__tests__/azure-mstts-namespace.test.ts +++ b/src/__tests__/azure-mstts-namespace.test.ts @@ -128,5 +128,26 @@ describe("Azure MSTTS Namespace Handling", () => { const xmlnsMatches = result.match(/xmlns="http:\/\/www\.w3\.org\/2001\/10\/synthesis"/g); expect(xmlnsMatches?.length).toBe(1); }); + + it("should nest inside , not as a direct child of ", async () => { + // Regression test for: https://github.com/willwade/js-tts-wrapper/issues/38 + // When rate/pitch/volume are passed as options, was placed outside + // , which Azure rejects with: + // "Node [speak] with type [RootSpeak] should not contain node [prosody] with type [Others]" + const plainSSML = `Hello world`; + const options = { rate: "fast", pitch: "high", volume: 80 }; + + const result = (client as any).ensureAzureSSMLStructure(plainSSML, "en-US-JennyNeural", options); + + // must appear after , not before it + const voiceIndex = result.indexOf("... + expect(result).toMatch(/]*>\s*]*>/); + expect(result).toMatch(/<\/prosody>\s*<\/voice>/); + }); }); }); diff --git a/src/engines/azure.ts b/src/engines/azure.ts index 4884c21..421273d 100644 --- a/src/engines/azure.ts +++ b/src/engines/azure.ts @@ -646,12 +646,22 @@ export class AzureTTSClient extends AbstractTTSClient { if (options.volume !== undefined) attrs.push(`volume="${options.volume}%"`); if (attrs.length > 0) { - // Extract content - const match = ssml.match(/]*>(.*?)<\/speak>/s); - if (match) { - const content = match[1]; - const prosodyContent = `${content}`; - ssml = ssml.replace(content, prosodyContent); + // Extract content from inside if present, otherwise from . + // Prosody must be nested inside , not as a direct child of . + if (ssml.includes("]*>(.*?)<\/voice>/s); + if (match) { + const content = match[1]; + const prosodyContent = `${content}`; + ssml = ssml.replace(content, prosodyContent); + } + } else { + const match = ssml.match(/]*>(.*?)<\/speak>/s); + if (match) { + const content = match[1]; + const prosodyContent = `${content}`; + ssml = ssml.replace(content, prosodyContent); + } } } }