欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

基于微软认知服务的文本到语音解决方案:JavaScript 实现

最编程 2024-05-22 08:32:09
...
/** * @file TextToSpeech.js * @description 该文件包含 TextToSpeech 类,用于将文本转换为语音。 * @version 1.0.0 */ import * as sdk from 'microsoft-cognitiveservices-speech-sdk'; import axios from 'axios'; // 选项映射表,用于将选项名称映射到 Speech SDK 属性 ID 或 speechSynthesis 属性名称 const OPTION_MAP = { 'language': { prop: 'speechSynthesisLanguage' // 语言 }, 'voiceName': { prop: 'speechSynthesisVoiceName' // 语音名称 }, 'outputFormat': { prop: 'speechSynthesisOutputFormat' // 输出格式 }, 'rate': { prop: sdk.PropertyId.SpeechServiceConnection_SynthSpeak_SsmlMaxRate // 语速 }, 'pitch': { prop: sdk.PropertyId.SpeechServiceConnection_SynthSpeak_SsmlMinRate // 音调 }, 'volume': { prop: sdk.PropertyId.SpeechServiceConnection_SynthVolume // 音量 } }; /** * TextToSpeech 类,用于将文本转换为语音。 */ export default class TextToSpeech { /** * 构造函数,创建 TextToSpeech 实例。 * @param {string} subscriptionKey - Azure 认知服务的订阅密钥。 * @param {string} serviceRegion - Azure 认知服务的区域。 * @param {number} bufferSize - 实时转换的最大缓存长度,太小会导致频繁触发tts服务, * @param {Object} options - 可选参数,用于配置 TextToSpeech 实例。 */ constructor(subscriptionKey, serviceRegion, bufferSize = 10, options = {}) { this.subscriptionKey = subscriptionKey; this.serviceRegion = serviceRegion; this.speechConfig = sdk.SpeechConfig.fromSubscription(subscriptionKey, serviceRegion); this.configure(options); this.voices = []; // 初始化缓冲区和缓冲区大小 this.bufferSize = bufferSize; // 初始化重试次数和最大重试次数 this.retryCount = 0; this.maxRetries = 3; // 使用 Proxy 监听 buffer 的变化 this.bufferObj = { text: '' }; // 使用对象来包装字符串 // 使用 Proxy 监听 bufferObj 的变化 this.bufferObj = new Proxy(this.bufferObj, { set: (target, property, value) => { // console.log(property, value) if (property === 'text') { target[property] = value; console.log(value.length, this.bufferSize, '999999') if (value.length >= this.bufferSize) { this.speak(value).then(() => { target[property] = ''; // 清空缓冲区 }); } } return true; } }); } /** * 添加文本到缓冲区。 * @param {string} text - 要添加到缓冲区的文本。 */ addToBuffer(text) { console.log(text) this.bufferObj.text += text; // 更新 Proxy 对象的 text 属性 } /** * 静态方法,异步创建 TextToSpeech 实例。 * @param {string} subscriptionKey - Azure 认知服务的订阅密钥。 * @param {string} serviceRegion - Azure 认知服务的区域。 * @param {Object} options - 可选参数,用于配置 TextToSpeech 实例。 * @returns {Promise<TextToSpeech>} - 返回一个 Promise,该 Promise 在 TextToSpeech 实例创建完成后解析为该实例。 */ static async build(subscriptionKey, serviceRegion, bufferSize, options = {}) { const instance = new TextToSpeech(subscriptionKey, serviceRegion, bufferSize, options); await instance.init(); return instance; } /** * 初始化 TextToSpeech 实例,获取可用的语音列表。 * @returns {Promise<void>} - 返回一个 Promise,该 Promise 在初始化完成后解析为 undefined。 */ async init() { if (!this.subscriptionKey || !this.serviceRegion) { console.error('Invalid configuration: subscriptionKey and serviceRegion are required.'); throw new Error('Invalid configuration'); } try { await this.fetchVoices(); this.filterVoicesByLanguage(this.speechConfig.speechSynthesisLanguage); } catch (error) { console.error('Failed to initialize TextToSpeech:', error); throw error; } } /** * 释放 TextToSpeech 实例占用的资源。 */ dispose() { // 在此处释放任何资源(如果适用) console.log('Resources are released.'); } /** * 获取可用的语音列表。 * @returns {Promise<Array>} - 返回一个 Promise,该 Promise 在获取完成后解析为语音列表。 */ async fetchVoices() { try { const url = `https://${this.serviceRegion}.tts.speech.microsoft.com/cognitiveservices/voices/list`; const headers = { 'Ocp-Apim-Subscription-Key': this.subscriptionKey }; const response = await axios.get(url, { headers }); this.voices = response.data; return this.voices; } catch (error) { console.error('Failed to fetch voices:', error); throw error; } } /** * 根据语言过滤语音列表。 * @param {string} language - 语言代码。 * @returns {Array} - 返回过滤后的语音列表。 */ filterVoicesByLanguage(language) { return this.voices.filter(voice => voice.Locale.startsWith(language)); } /** * 配置 TextToSpeech 实例。 * @param {Object} options - 可选参数,用于配置 TextToSpeech 实例。 */ configure(options = {}) { console.log(options) if (!options || typeof options !== 'object') { console.error('Invalid options argument:', options); throw new Error('Invalid options argument'); } Object.keys(options).forEach((key) => { const setting = OPTION_MAP[key]; if (setting) { if (typeof setting.prop === 'string') { this.speechConfig[setting.prop] = options[key]; } else { this.speechConfig.setProperty(setting.prop, options[key].toString()); } console.log(`Configured ${key} as ${options[key]}`); } else { console.warn(`Unknown configuration key: ${key}`); } }); } /** * 生成 SSML(Speech Synthesis Markup Language)文本。 * @param {string} text - 要转换为语音的文本。 * @param {string} style - 可选参数,用于指定语音样式。 * @returns {string} - 返回生成的 SSML 文本。 */ generateSsml(text, style = null) { let ssml = `<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='${this.speechConfig.speechSynthesisLanguage}'>`; ssml += `<voice name='${this.speechConfig.speechSynthesisVoiceName}'>`; if (style) { ssml += `<mstts:express-as style="${style}">`; } ssml += text; if (style) { ssml += `</mstts:express-as>`; } ssml += `</voice></speak>`; return ssml; } /** * 如果语音合成失败,则重试或拒绝。 * @param {Error} error - 语音合成失败的错误对象。 * @param {string} text - 要合成的文本。 * @param {Object} style - 合成的样式选项。 * @param {function} reject - Promise 的 reject 函数。 */ async retryOrReject(error, text, style, reject) { if (this.retryCount < this.maxRetries) { this.retryCount++; console.warn(`Synthesis failed, retrying... (${this.retryCount}/${this.maxRetries})`); await this.speak(text, style); // 重试 } else { this.retryCount = 0; // 重置重试次数 reject(`Synthesis failed after ${this.maxRetries} retries. ${error}`); } } /** * 将文本转换为语音。 * @param {string} text - 要转换为语音的文本。 * @param {string} style - 可选参数,用于指定语音样式。 * @returns {Promise<string>} - 返回一个 Promise,该 Promise 在转换完成后解析为字符串。 */ async speak(text, style = null) { const audioConfig = sdk.AudioConfig.fromDefaultSpeakerOutput(); const synthesizer = new sdk.SpeechSynthesizer(this.speechConfig, audioConfig); const ssml = this.generateSsml(text, style); console.log(ssml) return new Promise((resolve, reject) => { synthesizer.speakSsmlAsync(ssml, result => { if (result.reason === sdk.ResultReason.SynthesizingAudioCompleted) { this.retryCount = 0; // 重置重试次数 resolve("Synthesis succeeded."); } else { this.retryOrReject(result.errorDetails, text, style, reject); } synthesizer.close(); }, error => { this.retryOrReject(error, text, style, reject); synthesizer.close(); }); }); } /** * 将大文本分块处理,避免一次性转换过多文本导致性能问题。 * @param {string} text - 要转换为语音的文本。 * @param {number} maxChunkSize - 可选参数,用于指定每个块的最大长度。 * @returns {Promise<void>} - 返回一个 Promise,该 Promise 在转换完成后解析为 undefined。 */ async processLargeText(text, maxChunkSize = 5000) { const sentenceEnders = /[\.\?!]/g; let lastEnderIndex = 0; let offset = 0; const length = text.length; while (offset < length) { sentenceEnders.lastIndex = offset + maxChunkSize; // 从此处开始搜索 let sentenceEnd = sentenceEnders.exec(text); if (!sentenceEnd && offset + maxChunkSize >= length) { sentenceEnd = { index: length - 1 }; } if (sentenceEnd) { lastEnderIndex = sentenceEnd.index; const chunk = text.substring(offset, lastEnderIndex + 1); await this.speak(chunk.trim()); offset = lastEnderIndex + 1; } else { const chunk = text.substring(offset, offset + maxChunkSize); await this.speak(chunk.trim()); offset += maxChunkSize; } } } }