soarli

实现基于微软TTS的语音播报功能
本文由AI辅助撰写,可能存在不准确之处,请读者注意甄别!在这篇博客中,我将详细介绍如何使用HTML, JavaSc...
扫描右侧二维码阅读全文
28
2023/12

实现基于微软TTS的语音播报功能

本文由AI辅助撰写,可能存在不准确之处,请读者注意甄别!

在这篇博客中,我将详细介绍如何使用HTML, JavaScript和Vue.js来实现一个基于Web的语音播报(TTS,Text-To-Speech)示例。这个示例允许用户触发播放文本、暂停/继续播放和停止播放操作。我们将使用微软的Azure TTS服务来将文本实时转为语音,再进行播放。

初始HTML结构和Vue.js集成

首先,我们的HTML文档以标准的<!DOCTYPE html>声明开始,定义了一个简单的用户接口,它有三个按钮来控制文本的播放、暂停和停止。我们的HTML结构如下所示:

<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="UTF-8">
    <title>语音播报示例</title>
    <script src="https://unpkg.com/vue@3.2.16/dist/vue.global.prod.js"></script>
</head>
<body>
    <div id="app">
        <button @click="speak">播放文本</button>
        <button @click="pause">暂停/继续</button>
        <button @click="stop">停止</button>
    </div>
    <!-- 这里会接着放置我们的Vue和JavaScript代码 -->
</body>
</html>

这里我们包含了Vue.js,使用了它的全球CDN链接来轻松集成。

Vue.js应用构建

接下来,是创建Vue.js应用的脚本部分。我们从构建Vue应用开始,定义了一个state对象来存储我们需要的各种属性。

const { createApp, reactive, ref } = Vue;

createApp({
    setup() {
        const state = reactive({
            text: '你好,世界!',
            pitch: 0,
            rate: 30,
            volume: 0,
            lan: 'zh-CN',
            voice: 0,
        });

        // ...更多代码将在后面添加
    }
}).mount('#app');

在这个state对象中,我们存储了要朗读的文本、语音的音高(pitch)、速率(rate)、音量(volume)、使用的语言(lan)还有特定的语音代号(voice)。

与微软Azure TTS服务集成

要实现文本到语音的功能,我们需要和一个TTS服务进行通信。下面的例子中,示范如何与微软的Azure TTS服务通信:

function speak() {
    const bufferList = [];
    const ws = new WebSocket("wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1...");
    // ...其他逻辑
}

speak方法中,我们新建了一个WebSocket连接到微软的TTS服务,并在连接打开后发送了两个消息:speechConfigssmlText,后者是根据给定状态构造的SSML(Speech Synthesis Markup Language)。

发送语音配置和SSML请求文本

我们先定义一个函数生成SSML请求和配置语音的请求头部。

const textEncoder = new TextEncoder();
const binaryHeadEnd = textEncoder.encode('Path:audio\r\n').toString();

const speechConfig = () => {
    // ...配置信息
};

const ssmlText = (state, localVoiceList) => {
    // ...SSML请求文本
};

其中,ssmlText函数负责建立一个XML格式的消息体,它定义了将要转换成语音的文本,语音参数等。

WebSocket通信和事件处理

我们监听WebSocket的几个关键事件,例如message事件,这是我们处理从服务器接收到的二进制音频数据的地方。

ws.addEventListener('message', async ({ data }) => {
    // ...处理接收到的音频数据
});

ws.addEventListener('error', (err) => {
    // ...处理错误事件
});

ws.addEventListener('close', () => {
    // ...处理连接关闭事件
});

当接收到消息时,我们将音频数据的二进制拼接起来。最终在连接关闭事件中,用Blob对象创建一个音频URL,并且用Audio对象来播放它。

控制音频播放

我们创建函数来控制音频的播放,包括暂停和停止功能。

function pause() {
    // 暂停或继续播放音频
}

function stop() {
    // 停止音频的播放
}

speak函数结束时,我们返回了这些控制函数,这样它们就可以在Vue的模板中以方法的形式被绑定到按钮的点击事件上。最终Vue的setup函数如下:

setup() {
    // ...初始化state和其他响应式数据

    // 定义speak, pause, stop函数

    return {
        speak,
        pause,
        stop,
    };
}

完整代码

<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="UTF-8">
    <title>语音播报示例</title>
    <script src="https://unpkg.com/vue@3.2.16/dist/vue.global.prod.js"></script>
</head>
<body>
    <div id="app">
      <button @click="speak">播放文本</button>
      <button @click="pause">暂停/继续</button>
      <button @click="stop">停止</button>
    </div>

    <script>
        const { createApp, reactive, ref } = Vue;

        const textEncoder = new TextEncoder();
        const binaryHeadEnd = textEncoder.encode('Path:audio\r\n').toString();

        const speechConfig = () => `X-Timestamp:${new Date().toISOString()}\r\nContent-Type:application/json; charset=utf-8\r\nPath:speech.config\r\n\r\n{"context":{"synthesis":{"audio":{"metadataoptions":{"sentenceBoundaryEnabled":"false","wordBoundaryEnabled":"true"},"outputFormat":"webm-24khz-16bit-mono-opus"}}}}`;

        const ssmlText = (state, localVoiceList) => {
            const requestId = guid();
            return `X-RequestId:${requestId}\r\nContent-Type:application/ssml+xml\r\nX-Timestamp:${new Date().toISOString()}\r\nPath:ssml\r\n\r\n<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xmlns:mstts='https://www.w3.org/2001/mstts' xml:lang='${state.lan}'><voice name='${localVoiceList[state.lan][state.voice].Name}'><prosody pitch='${numToString(state.pitch)}Hz' rate ='${numToString(state.rate)}%' volume='${numToString(state.volume)}%'>${state.text}</prosody></voice></speak>`;
        };

        function guid(){
            function gen(count) {
                var out = "";
                for (var i = 0; i < count; i++) {
                    out += (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
                }
                return out;
            }
            return gen(8);
        }
        
        function numToString(num) {
            return num >= 0 ? `+${num}` : `${num}`;
        }

        createApp({
            setup() {
                const state = reactive({
                    text: '你好,世界!我能为你做什么?',
                    pitch: 0,
                    rate: 30,
                    volume: 0,
                    lan: 'zh-CN',
                    voice: 0,
                });

                const localVoiceList = {
                    'zh-CN': [{ 'Name': 'zh-CN-XiaoxiaoNeural' }],
                };

                // 创建一个响应式引用来跟踪音频对象
                const audio = ref(null);

                function speak() {
                    const bufferList = [];
                    const ws = new WebSocket("wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1?TrustedClientToken=6A5AA1D4EAFF4E9FB37E23D68491D6F4");

                    ws.addEventListener('open', () => {
                        ws.send(speechConfig());
                        ws.send(ssmlText(state, localVoiceList));
                    });

                    ws.addEventListener('message', async ({ data }) => {
                        if (data instanceof Blob) {
                            const view = new Uint8Array(await data.arrayBuffer());
                            bufferList.push(...view.toString().split(binaryHeadEnd)[1].split(',').slice(1).map(i => +i));
                            if(view[0] == 0x00 && view[1] == 0x67 && view[2] == 0x58) {
                                ws.close(1000);
                            }
                        }
                    });

                    ws.addEventListener("error", (err) => {
                        console.error('WebSocket error', err);
                    });

                    ws.addEventListener('close', () => {
                        if(bufferList.length) {
                            const blob = new Blob([new Uint8Array(bufferList)], {type: 'audio/webm'});
                            const audioUrl = URL.createObjectURL(blob);
                            // 将Audio对象保存在Vue实例中
                            audio.value = new Audio(audioUrl);
                            audio.value.play();
                        } else {
                            console.error('No audio data received.');
                        }
                    });
                }

                // 暂停/继续播放的功能
                function pause() {
                    if (audio.value) {
                        if (audio.value.paused) {
                            audio.value.play();
                        } else {
                            audio.value.pause();
                        }
                    }
                }

                // 停止播放的功能
                function stop() {
                    if (audio.value) {
                        audio.value.pause(); // 暂停播放
                        audio.value.currentTime = 0; // 重置时间
                    }
                }

                return {
                    speak,
                    pause,
                    stop,
                };
            }
        }).mount('#app');
    </script>
</body>
</html>

结语

通过这个实现,我们展示了如何将文本转换为语音并在Web应用中播放。我们涉及到了Vue.js的基本应用构建、与第三方API集成的WebSocket通信以及HTML5的音频API的使用。这个基础示例可以被应用于更复杂的场景,比如构建一个完整的Web语音读取器,或者集成到聊天机器人中提升交互体验。希望这篇博客对你的项目有启发和帮助!

最后修改:2023 年 12 月 29 日 03 : 30 AM

发表评论