diff --git a/sdk/csharp/libraries/microsoft.bot.solutions/Middleware/SetSpeakMiddleware.cs b/sdk/csharp/libraries/microsoft.bot.solutions/Middleware/SetSpeakMiddleware.cs deleted file mode 100644 index 2ccfd3f216..0000000000 --- a/sdk/csharp/libraries/microsoft.bot.solutions/Middleware/SetSpeakMiddleware.cs +++ /dev/null @@ -1,246 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Bot.Solutions.Middleware -{ - using System; - using System.Collections.Generic; - using System.Globalization; - using System.Threading; - using System.Threading.Tasks; - using System.Xml; - using System.Xml.Linq; - using Microsoft.Bot.Builder; - using Microsoft.Bot.Schema; - using Newtonsoft.Json.Linq; - - /// - /// Set Speech Synthesis Markup Language (SSML) on an Activity's Speak property with locale and voice input. - /// - public class SetSpeakMiddleware : IMiddleware - { - private const string DefaultLocale = "en-US"; - - private static readonly IDictionary DefaultVoiceFonts = new Dictionary() - { - { "de-DE", "Microsoft Server Speech Text to Speech Voice (de-DE, Hedda)" }, - { "en-US", "Microsoft Server Speech Text to Speech Voice (en-US, Jessa24kRUS)" }, - { "es-ES", "Microsoft Server Speech Text to Speech Voice (es-ES, Laura, Apollo)" }, - { "fr-FR", "Microsoft Server Speech Text to Speech Voice (fr-FR, Julie, Apollo)" }, - { "it-IT", "Microsoft Server Speech Text to Speech Voice (it-IT, Cosimo, Apollo)" }, - { "zh-CN", "Microsoft Server Speech Text to Speech Voice (zh-CN, HuihuiRUS)" }, - }; - - private static readonly ISet DefaultChannels = new HashSet() - { - Connector.Channels.DirectlineSpeech, - Connector.Channels.Emulator, - }; - - private static string _locale; - - private static IDictionary _voiceFonts; - - private static ISet _channels; - - private static readonly XNamespace NamespaceURI = @"https://www.w3.org/2001/10/synthesis"; - - /// - /// Initializes a new instance of the class. - /// - /// If null, use en-US. - /// Map voice font for locale like en-US to "Microsoft Server Speech Text to Speech Voice (en-US, Jessa24kRUS)". - /// Set SSML for these channels. If null, use and . - public SetSpeakMiddleware(string locale = null, IDictionary voiceFonts = null, ISet channels = null) - { - _locale = locale ?? DefaultLocale; - _voiceFonts = voiceFonts ?? DefaultVoiceFonts; - _channels = channels ?? DefaultChannels; - } - - /// - /// If outgoing Activities are messages and using one of the desired channels, decorate the Speak property with an SSML formatted string. - /// - /// The Bot Context object. - /// The next middleware component to run. - /// The cancellation token for the task. - /// A representing the asynchronous operation. - public Task OnTurnAsync(ITurnContext context, NextDelegate next, CancellationToken cancellationToken = default(CancellationToken)) - { - context.OnSendActivities(async (ctx, activities, nextSend) => - { - foreach (var activity in activities) - { - switch (activity.Type) - { - case ActivityTypes.Message: - activity.Speak = GetActivitySpeakText(activity); - - if (_channels.Contains(activity.ChannelId)) - { - activity.Speak = DecorateSSML(activity); - } - - break; - } - } - - return await nextSend().ConfigureAwait(false); - }); - - return next(cancellationToken); - } - - /// - /// Gets the speak text for the activity. - /// - /// Outgoing bot Activity. - /// speech text string value. - private static string GetActivitySpeakText(Activity activity) - { - // return speak or text value if they already exist in the activity - var result = activity.Speak ?? activity.Text; - if (result != null) - { - return result; - } - - // return speak value of first attachment if an attachment exists and has a speak value - if (activity.Attachments.Count > 0) - { - var attachmentContent = activity.Attachments[0].Content; - if (attachmentContent != null) - { - var contentObj = attachmentContent as JObject; - return contentObj?["speak"]?.ToString(); - } - } - - return null; - } - - /// - /// Formats an existing string to be formatted for Speech Synthesis Markup Language with a voice font. - /// - /// Outgoing bot Activity. - /// SSML-formatted string to be used with synthetic speech. - private static string DecorateSSML(Activity activity) - { - if (string.IsNullOrWhiteSpace(activity.Speak)) - { - return activity.Speak?.Trim(); - } - - XElement rootElement = null; - try - { - rootElement = XElement.Parse(activity.Speak); - } - catch (XmlException) - { - // Ignore any exceptions. This is effectively a "TryParse", except that XElement doesn't - // have a TryParse method. - } - - if (rootElement == null || rootElement.Name.LocalName != "speak") - { - // If the text is not valid XML, or if it's not a node, treat it as plain text. - rootElement = new XElement(NamespaceURI + "speak", activity.Speak); - } - - var locale = _locale; - if (!string.IsNullOrEmpty(activity.Locale)) - { - try - { - var normalizedLocale = new CultureInfo(activity.Locale).Name; - if (_voiceFonts.ContainsKey(normalizedLocale)) - { - locale = normalizedLocale; - } - } - catch (CultureNotFoundException) - { - } - } - - AddAttributeIfMissing(rootElement, "version", "1.0"); - AddAttributeIfMissing(rootElement, XNamespace.Xml + "lang", locale); - AddAttributeIfMissing(rootElement, XNamespace.Xmlns + "mstts", "https://www.w3.org/2001/mstts"); - - var sayAsElements = rootElement.Elements("say-as"); - foreach (var element in sayAsElements) - { - UpdateAttributeIfPresent(element, "interpret-as", "digits", "number_digit"); - } - - // add voice element if absent - AddVoiceElementIfMissing(rootElement, _voiceFonts[locale]); - - return rootElement.ToString(SaveOptions.DisableFormatting); - } - - /// - /// Add a new attribute to an XML element. - /// - /// The XML element to update. - /// The XML attribute name to add. - /// The XML attribute value to add. - private static void AddAttributeIfMissing(XElement element, XName attributeName, string attributeValue) - { - var existingAttribute = element.Attribute(attributeName); - if (existingAttribute == null) - { - element.Add(new XAttribute(attributeName, attributeValue)); - } - } - - /// - /// Add a new attribute with a voice property to the parent XML element. - /// - /// The XML element to update. - /// The XML attribute to add. - private static void AddVoiceElementIfMissing(XElement parentElement, string attributeValue) - { - try - { - var existingVoiceElement = parentElement.Element("voice") ?? parentElement.Element(NamespaceURI + "voice"); - - // If an existing voice element is null (absent), then add it. Otherwise, assume the author has set it correctly. - if (existingVoiceElement == null) - { - var existingNodes = parentElement.DescendantNodes(); - - XElement voiceElement = new XElement("voice", new XAttribute("name", attributeValue)); - voiceElement.Add(existingNodes); - parentElement.RemoveNodes(); - parentElement.Add(voiceElement); - } - else - { - existingVoiceElement.SetAttributeValue("name", attributeValue); - } - } - catch (Exception ex) - { - throw new Exception($"Could not add voice element to speak property {ex.Message}"); - } - } - - /// - /// Update an XML attribute if it already exists. - /// - /// The XML element to update. - /// The XML attribute name to update. - /// The current XML attribute's value. - /// The new XML attribute's value. - private static void UpdateAttributeIfPresent(XElement element, XName attributeName, string currentAttributeValue, string newAttributeValue) - { - var existingAttribute = element?.Attribute(attributeName); - if (existingAttribute?.Value == currentAttributeValue) - { - existingAttribute?.SetValue(newAttributeValue); - } - } - } -} \ No newline at end of file diff --git a/sdk/csharp/tests/microsoft.bot.solutions.tests/Middleware/SetSpeakMiddlewareTests.cs b/sdk/csharp/tests/microsoft.bot.solutions.tests/Middleware/SetSpeakMiddlewareTests.cs deleted file mode 100644 index 7406b918bd..0000000000 --- a/sdk/csharp/tests/microsoft.bot.solutions.tests/Middleware/SetSpeakMiddlewareTests.cs +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; -using System.Xml.Linq; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Adapters; -using Microsoft.Bot.Schema; -using Microsoft.Bot.Solutions.Middleware; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Microsoft.Bot.Solutions.Tests.Middleware -{ - [TestClass] - [TestCategory("UnitTests")] - [ExcludeFromCodeCoverageAttribute] - public class SetSpeakMiddlewareTests - { - [TestMethod] - public async Task DefaultOptions_Default() - { - var storage = new MemoryStorage(); - var convState = new ConversationState(storage); - - var conversation = TestAdapter.CreateConversation("Name"); - conversation.ChannelId = Connector.Channels.DirectlineSpeech; - - var adapter = new TestAdapter(conversation) - .Use(new SetSpeakMiddleware()); - adapter.Locale = string.Empty; - - var response = "Response"; - - await new TestFlow(adapter, async (context, cancellationToken) => - { - await context.SendActivityAsync(context.Activity.CreateReply(response)); - }) - .Send("foo") - .AssertReply((reply) => - { - var activity = (Activity)reply; - var rootElement = XElement.Parse(activity.Speak); - Assert.AreEqual(rootElement.Name.LocalName, "speak"); - Assert.AreEqual(rootElement.Attribute(XNamespace.Xml + "lang").Value, "en-US"); - var voiceElement = rootElement.Element("voice"); - Assert.AreEqual(voiceElement.Attribute("name").Value, "Microsoft Server Speech Text to Speech Voice (en-US, Jessa24kRUS)"); - Assert.AreEqual(voiceElement.Value, response); - }) - .StartTestAsync(); - } - - [TestMethod] - public async Task DefaultOptions_Invalid() - { - var storage = new MemoryStorage(); - var convState = new ConversationState(storage); - - var conversation = TestAdapter.CreateConversation("Name"); - conversation.ChannelId = Connector.Channels.DirectlineSpeech; - - var adapter = new TestAdapter(conversation) - .Use(new SetSpeakMiddleware()); - adapter.Locale = "InvalidLocale"; - - var response = "Response"; - - await new TestFlow(adapter, async (context, cancellationToken) => - { - await context.SendActivityAsync(context.Activity.CreateReply(response)); - }) - .Send("foo") - .AssertReply((reply) => - { - var activity = (Activity)reply; - var rootElement = XElement.Parse(activity.Speak); - Assert.AreEqual(rootElement.Name.LocalName, "speak"); - Assert.AreEqual(rootElement.Attribute(XNamespace.Xml + "lang").Value, "en-US"); - var voiceElement = rootElement.Element("voice"); - Assert.AreEqual(voiceElement.Attribute("name").Value, "Microsoft Server Speech Text to Speech Voice (en-US, Jessa24kRUS)"); - Assert.AreEqual(voiceElement.Value, response); - }) - .StartTestAsync(); - } - - [TestMethod] - public async Task DefaultOptions_IncorrectCase() - { - var storage = new MemoryStorage(); - var convState = new ConversationState(storage); - - var conversation = TestAdapter.CreateConversation("Name"); - conversation.ChannelId = Connector.Channels.DirectlineSpeech; - - var adapter = new TestAdapter(conversation) - .Use(new SetSpeakMiddleware()); - adapter.Locale = "zh-cn"; - - var response = "Response"; - - await new TestFlow(adapter, async (context, cancellationToken) => - { - await context.SendActivityAsync(context.Activity.CreateReply(response)); - }) - .Send("foo") - .AssertReply((reply) => - { - var activity = (Activity)reply; - var rootElement = XElement.Parse(activity.Speak); - Assert.AreEqual(rootElement.Name.LocalName, "speak"); - Assert.AreEqual(rootElement.Attribute(XNamespace.Xml + "lang").Value, "zh-CN"); - var voiceElement = rootElement.Element("voice"); - Assert.AreEqual(voiceElement.Attribute("name").Value, "Microsoft Server Speech Text to Speech Voice (zh-CN, HuihuiRUS)"); - Assert.AreEqual(voiceElement.Value, response); - }) - .StartTestAsync(); - } - } -} diff --git a/sdk/typescript/libraries/bot-solutions/src/middleware/setSpeakMiddleware.ts b/sdk/typescript/libraries/bot-solutions/src/middleware/setSpeakMiddleware.ts deleted file mode 100644 index f3521e0b9c..0000000000 --- a/sdk/typescript/libraries/bot-solutions/src/middleware/setSpeakMiddleware.ts +++ /dev/null @@ -1,254 +0,0 @@ -/** - * Copyright(c) Microsoft Corporation.All rights reserved. - * Licensed under the MIT License. - */ - -import { Middleware, SendActivitiesHandler, TurnContext } from 'botbuilder'; -import { Activity, ActivityTypes, ResourceResponse, Channels } from 'botframework-schema'; -import { Element, js2xml, xml2js } from 'xml-js'; - -/** - * Set Speech Synthesis Markup Language (SSML) on an Activity's Speak property with locale and voice input. - */ -export class SetSpeakMiddleware implements Middleware { - private static readonly defaultLocale: string = 'en-us'; - private static readonly defaultVoiceFonts: Map = new Map([ - ['de-de', 'Microsoft Server Speech Text to Speech Voice (de-DE, Hedda)'], - ['en-us', 'Microsoft Server Speech Text to Speech Voice (en-US, Jessa24kRUS)'], - ['es-es', 'Microsoft Server Speech Text to Speech Voice (es-ES, Laura, Apollo)'], - ['fr-fr', 'Microsoft Server Speech Text to Speech Voice (fr-FR, Julie, Apollo)'], - ['it-it', 'Microsoft Server Speech Text to Speech Voice (it-IT, Cosimo, Apollo)'], - ['zh-cn', 'Microsoft Server Speech Text to Speech Voice (zh-CN, HuihuiRUS)'] - ]); - private static readonly defaultChannels: Set = new Set([Channels.DirectlineSpeech, Channels.Emulator]); - private locale: string; - private voiceFonts: Map; - private channels: Set; - private static readonly namespaceURI = 'https://www.w3.org/2001/10/synthesis'; - - /** - * Initializes a new instance of the SetSpeakMiddleware class. - * @param locale If null, use en-US. - * @param voiceFonts Map voice font for locale like en-US to "Microsoft Server Speech Text to Speech Voice (en-us, Jessa24kRUS). - * @param channels Set SSML for these channels. If null, use DirectlineSpeech and Emulator. - */ - public constructor(locale: string = '', voiceFonts: Map = new Map(), channels: Set = new Set()) { - this.locale = locale || SetSpeakMiddleware.defaultLocale; - this.voiceFonts = voiceFonts.size > 0 ? voiceFonts : SetSpeakMiddleware.defaultVoiceFonts; - this.channels = channels.size > 0 ? channels : SetSpeakMiddleware.defaultChannels; - } - - /** - * If outgoing Activities are messages and using the Direct Line Speech channel, - * decorate the Speak property with an SSML formatted string. - * @param context The Bot Context object. - * @param next The next middleware component to run. - */ - public async onTurn(context: TurnContext, next: () => Promise): Promise { - const handler: SendActivitiesHandler = async ( - ctx: TurnContext, activities: Partial[], nextSend: () => Promise - ): Promise => { - activities.forEach((activity: Partial): void => { - switch (activity.type) { - case ActivityTypes.Message: { - activity.speak = this.getActivitySpeakText(activity); - - if (activity.channelId !== undefined) { - if (this.channels.has(activity.channelId)) { - activity.speak = this.decorateSSML(activity); - } - } - break; - } - default: - } - }); - - return await nextSend(); - }; - - context.onSendActivities(handler); - - return next(); - } - - /** - * Gets the speak text for the activity. - * @param activity Outgoing bot Activity. - * @returns speech text string value. - */ - private getActivitySpeakText(activity: Partial): string { - // return speak or text value if they already exist in the activity - const result: string | undefined = activity.speak || activity.text; - if (result !== undefined) { - return result; - } - - // return speak value of first attachment if an attachment exists and has a speak value - if (activity.attachments !== undefined && activity.attachments.length > 0) { - const attachmentContent = activity.attachments[0].content; - if (attachmentContent !== undefined) { - const contentObj = attachmentContent; - if(contentObj['speak'] == undefined) - { - return ''; - } - return contentObj['speak'].toString(); - } - } - - return ''; - } - - /** - * Formats an existing string to be formatted for Speech Synthesis Markup Language with a voice font. - * @param activity Outgoing bot Activity. - * @returns SSML-formatted string to be used with synthetic speech. - */ - private decorateSSML(activity: Partial): string { - if (activity.speak === undefined || activity.speak.trim().length === 0) { - return ''; - } - - let rootElement: Element | undefined = undefined; - try { - rootElement = xml2js(activity.speak, { compact: false }) as Element; - } catch(err){ - // Ignore any exceptions. This is effectively a "TryParse", except that XElement doesn't - // have a TryParse method. - } - - if (rootElement === undefined || this.getLocalName(rootElement) !== 'speak') { - // If the text is not valid XML, or if it's not a node, treat it as plain text. - rootElement = this.createBaseElement(activity.speak); - } - - let locale = this.locale; - if (activity.locale !== undefined && activity.locale.trim().length > 0) { - try { - const normalizedLocale: string = activity.locale.toLowerCase(); - if (this.voiceFonts.has(normalizedLocale)){ - locale = normalizedLocale; - } - } catch(err) { - } - } - - this.addAttributeIfMissing(rootElement, 'version', '1.0'); - this.addAttributeIfMissing(rootElement, 'xml:lang', `${ locale }`); - this.addAttributeIfMissing(rootElement, 'xmlns:mstts', 'https://www.w3.org/2001/mstts'); - - // Fix issue with 'number_digit' interpreter - if (rootElement.elements !== undefined && rootElement.elements[0].elements !== undefined) { - const sayAsElements: Element[] = rootElement.elements[0].elements.filter((e: Element): boolean => e.name === 'say-as'); - sayAsElements.forEach((e: Element): void => { - this.updateAttributeIfPresent(e, 'interpret-as', 'digits', 'number_digit'); - }); - } - - // add voice element if absent - const voiceFontOfLocale: string | undefined = this.voiceFonts.get(locale); - if (voiceFontOfLocale !== undefined && voiceFontOfLocale.trim().length > 0) { - this.addVoiceElementIfMissing(rootElement, voiceFontOfLocale); - } - - return js2xml(rootElement, { compact: false }); - } - - /** - * Add a new attribute to an XML element. - * @param element The XML element to update. - * @param attributeName The XML attribute name to add. - * @param attributeValue The XML attribute value to add. - */ - private addAttributeIfMissing(element: Element, attributeName: string, attributeValue: string): void { - if (element.elements !== undefined && element.elements[0] !== undefined) { - if (element.elements[0].attributes === undefined) { - element.elements[0].attributes = { - attName: attributeValue - }; - } else { - if (element.elements[0].attributes[attributeName] === undefined) { - element.elements[0].attributes[attributeName] = attributeValue; - } - } - } - } - - /** - * Add a new attribute with a voice property to the parent XML element. - * @param parentElement The XML element to update. - * @param attributeValue The XML attribute value to add. - */ - private addVoiceElementIfMissing(parentElement: Element, attributeValue: string): void { - try { - if (parentElement.elements === undefined || parentElement.elements[0].elements === undefined) { - throw new Error('rootElement undefined'); - } - const existingVoiceElement: Element | undefined = parentElement.elements[0].elements.find((e: Element): boolean => e.name === 'voice'); - - // If an existing voice element is undefined (absent), then add it. Otherwise, assume the author has set it correctly. - if (existingVoiceElement === undefined) { - const oldElements: Element[] = JSON.parse(JSON.stringify(parentElement.elements[0].elements)); - parentElement.elements[0].elements = [ - { - type: 'element', - name: 'voice', - attributes: { - name: attributeValue - }, - elements: oldElements - } - ]; - } else { - if (existingVoiceElement.attributes !== undefined) { - existingVoiceElement.attributes['name'] = attributeValue; - } - } - } catch (error) { - throw new Error(`Could not add voice element to speak property: ${ error.message }`); - } - } - - /** - * Update an XML attribute if it already exists. - * @param element The XML element to update. - * @param attributeName The XML attribute name to update. - * @param currentAttributeValue The XML attribute name to update. - * @param newAttributeValue The XML attribute name to update. - */ - private updateAttributeIfPresent(element: Element, attributeName: string, currentAttributeValue: string, newAttributeValue: string): void { - if (element.attributes !== undefined && element.attributes[attributeName] === currentAttributeValue) { - element.attributes[attributeName] = newAttributeValue; - } - } - - private getLocalName(element: Element): string | undefined { - if (element.elements !== undefined && element.elements.length === 1) { - return element.elements[0].name; - } - - return undefined; - } - - private createBaseElement(value: string): Element { - // creating simple element - return { - elements: [ - { - type: 'element', - name: 'speak', - elements: [ - { - type: 'text', - text: value - } - ], - attributes: { - xmlns: SetSpeakMiddleware.namespaceURI - } - } - ] - }; - } -} diff --git a/sdk/typescript/libraries/bot-solutions/test/middleware/setSpeakMiddleware.test.js b/sdk/typescript/libraries/bot-solutions/test/middleware/setSpeakMiddleware.test.js deleted file mode 100644 index 498a58f6a6..0000000000 --- a/sdk/typescript/libraries/bot-solutions/test/middleware/setSpeakMiddleware.test.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright(c) Microsoft Corporation.All rights reserved. - * Licensed under the MIT License. - */ - -const { strictEqual } = require("assert"); -const { MemoryStorage, ConversationState, TestAdapter } = require("botbuilder"); -const { SetSpeakMiddleware } = require("../../lib/middleware"); -const { ActivityEx } = require("../../lib/extensions/activityEx"); -const { xml2js } = require("xml-js"); -const { Channels } = require("botframework-schema"); - -describe("setSpeak middleware", function() { - it("should default options to default", async function() { - const storage = new MemoryStorage(); - const convState = new ConversationState(storage); - - const response = "Response"; - - const testAdapter = new TestAdapter(async function(context) { - context.activity.channelId = Channels.DirectlineSpeech; - const reply = ActivityEx.createReply(context.activity, response, ''); - await context.sendActivity(reply); - }) - .use(new SetSpeakMiddleware()); - - await testAdapter.send("foo") - .assertReply((activity) => { - const elements = xml2js(activity.speak, { compact: false }); - const rootElement = elements.elements[0]; - strictEqual(rootElement.name, "speak"); - strictEqual(rootElement.attributes["xml:lang"], "en-us"); - const voiceElement = rootElement.elements[0]; - strictEqual(voiceElement.attributes.name, "Microsoft Server Speech Text to Speech Voice (en-US, Jessa24kRUS)"); - strictEqual(voiceElement.elements[0].text, response); - }) - .startTest(); - }); - - it("should default options to default", async function() { - const storage = new MemoryStorage(); - const convState = new ConversationState(storage); - - const response = "Response"; - - const testAdapter = new TestAdapter(async function(context) { - context.activity.channelId = Channels.DirectlineSpeech; - const reply = ActivityEx.createReply(context.activity, response, 'invalidLocale'); - await context.sendActivity(reply); - }) - .use(new SetSpeakMiddleware()); - - await testAdapter.send("foo") - .assertReply((activity) => { - const elements = xml2js(activity.speak, { compact: false }); - const rootElement = elements.elements[0]; - strictEqual(rootElement.name, "speak"); - strictEqual(rootElement.attributes["xml:lang"], "en-us"); - const voiceElement = rootElement.elements[0]; - strictEqual(voiceElement.attributes.name, "Microsoft Server Speech Text to Speech Voice (en-US, Jessa24kRUS)"); - strictEqual(voiceElement.elements[0].text, response); - }) - .startTest(); - }); - - it("should default options to incorrect case", async function() { - const storage = new MemoryStorage(); - const convState = new ConversationState(storage); - - const response = "Response"; - - const testAdapter = new TestAdapter(async function(context) { - context.activity.channelId = Channels.DirectlineSpeech; - const reply = ActivityEx.createReply(context.activity, response); - await context.sendActivity(reply); - }).use(new SetSpeakMiddleware('zh-cn')).send("foo") - .assertReply((activity) => { - const elements = xml2js(activity.speak, { compact: false }); - const rootElement = elements.elements[0]; - strictEqual(rootElement.name, "speak"); - strictEqual(rootElement.attributes["xml:lang"], "zh-cn"); - const voiceElement = rootElement.elements[0]; - strictEqual(voiceElement.attributes.name, "Microsoft Server Speech Text to Speech Voice (zh-CN, HuihuiRUS)"); - strictEqual(voiceElement.elements[0].text, response); - }) - .startTest(); - }); -});