在uni-app中使用微軟的文字轉語音服務

語言: CN / TW / HK

我正在參加跨端技術專題徵文活動,詳情查看:juejin.cn/post/710123…

前言

嘗試過各種TTS的方案,一番體驗下來,發現微軟才是這個領域的王者,其Azure文本轉語音服務的轉換出的語音效果最為自然,但Azure是付費服務,註冊操作付費都太麻煩了。但在其官網上竟然提供了一個完全體的演示功能,能夠完完整整的體驗所有角色語音,説話風格...

image.png

但就是不能下載成mp3文件,所以有一些小夥伴逼不得已只好通過轉錄電腦的聲音來獲得音頻文件,但這樣太麻煩了。其實,能在網頁裏看到聽到的所有資源,都是解密後的結果。也就是説,只要這個聲音從網頁裏播放出來了,我們必然可以找到方法提取到音頻文件。

本文就是記錄了這整個探索實現的過程,請盡情享用~

本文大部分內容寫於今年年初一直按在手裏未發佈,我深知這個方法一旦公之於眾,可能很快會迎來微軟的封堵,甚至直接取消網頁體驗的入口和相關接口。

解析Azure官網的演示功能

使用Chrome瀏覽器打開調試面板,當我們在Azure官網中點擊播放功能時,可以從network標籤中監控到一個wss://的請求,這是一個websocket的請求。

image.png

兩個參數

在請求的URL中,我們可以看到有兩個參數分別是AuthorizationX-ConnectionId

image.png

有意思的是,第一個參數就在網頁的源碼裏,使用axios對這個Azure文本轉語音的網址發起get請求就可以直接提取到

image.png

``` const res = await axios.get("https://azure.microsoft.com/en-gb/services/cognitive-services/text-to-speech/");

const reg = /token: \"(.*?)\"/;

if(reg.test(res.data)){ const token = RegExp.$1; } ```

通過查看發起請求的JS調用棧,加入斷點後再次點擊播放

image.png

image.png

可以發現第二個參數X-ConnectionId來自一個createNoDashGuid的函數

this.privConnectionId = void 0 !== t ? t : s.createNoDashGuid(),

這就是一個uuid v4格式的字符串,nodash就是沒有-的意思。

三次發送

請求時URL裏的兩個參數已經搞定了,我們繼續分析這個webscoket請求,從Message標籤中可以看到

image.png

每次點擊播放時,都向服務器上報了三次數據,明顯可以看出來三次上報數據各自的作用

第一次的數據:SDK版本,系統信息,UserAgent ``` Path: speech.config X-RequestId: 818A1E398D8D4303956D180A3761864B X-Timestamp: 2022-05-27T16:45:02.799Z Content-Type: application/json

{"context":{"system":{"name":"SpeechSDK","version":"1.19.0","build":"JavaScript","lang":"JavaScript"},"os":{"platform":"Browser/MacIntel","name":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36","version":"5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36"}}} ```

第二次的數據:轉語音輸出配置,從outputFormat可以看出來,最終的音頻格式為audio-24khz-160kbitrate-mono-mp3,這不就是我們想要的mp3文件嗎?! ``` Path: synthesis.context X-RequestId: 091963E8C7F342D0A8E79125EA6BB707 X-Timestamp: 2022-05-27T16:48:43.340Z Content-Type: application/json

{"synthesis":{"audio":{"metadataOptions":{"bookmarkEnabled":false,"sentenceBoundaryEnabled":false,"visemeEnabled":false,"wordBoundaryEnabled":false},"outputFormat":"audio-24khz-160kbitrate-mono-mp3"},"language":{"autoDetection":false}}} ```

第三次的數據:要轉語音的文本信息和角色voice name,語速rate,語調pitch,情感等配置 ``` Path: ssml X-RequestId: 091963E8C7F342D0A8E79125EA6BB707 X-Timestamp: 2022-05-27T16:48:49.594Z Content-Type: application/ssml+xml

我叫大帥,一個熱愛編程的老程序猿 ```

接收的二進制消息

既然從前三次上報的信息已經看出來返回的格式就是mp3文件了,那麼我們是不是把所有返回的二進制數據合併就可以拼接成完整的mp3文件了呢?答案是肯定的!

每次點擊播放後接收的所有來自websocket的消息的最後一條,都有明確的結束標識符

image.png

image.png

turn.end代表轉換結束!

用Node.js實現它

既然都解析出來了,剩下的就是在Node.js中重新實現這個過程。

兩個參數

  1. Authorization,直接通過axios的get請求抓取網頁內容後通過正則表達式提取

``` const res = await axios.get("https://azure.microsoft.com/en-gb/services/cognitive-services/text-to-speech/");

const reg = /token: \"(.*?)\"/;

if(reg.test(res.data)){ const Authorization = RegExp.$1; } ```

  1. X-ConnectionId,直接使用uuid庫即可

``` //npm install uuid const { v4: uuidv4 } = require('uuid');

const XConnectionId = uuidv4().toUpperCase(); ```

創建WebSocket連接

``` //npm install nodejs-websocket const ws = require("nodejs-websocket");

const url = wss://eastus.tts.speech.microsoft.com/cognitiveservices/websocket/v1?Authorization=${Authorization}&X-ConnectionId=${XConnectionId}; const connect = ws.connect(url); ```

三次發送

第一次發送 ``` function getXTime(){ return new Date().toISOString(); }

const message_1 = Path: speech.config\r\nX-RequestId: ${XConnectionId}\r\nX-Timestamp: ${getXTime()}\r\nContent-Type: application/json\r\n\r\n{"context":{"system":{"name":"SpeechSDK","version":"1.19.0","build":"JavaScript","lang":"JavaScript","os":{"platform":"Browser/Linux x86_64","name":"Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0","version":"5.0 (X11)"}}}};

connect.send(message_1); ```

第二次發送 `` const message_2 =Path: synthesis.context\r\nX-RequestId: ${XConnectionId}\r\nX-Timestamp: ${getXTime()}\r\nContent-Type: application/json\r\n\r\n{"synthesis":{"audio":{"metadataOptions":{"sentenceBoundaryEnabled":false,"wordBoundaryEnabled":false},"outputFormat":"audio-16khz-32kbitrate-mono-mp3"}}}`;

connect.send(message_2); ```

第三次發送 `` const SSML = 我叫大帥,一個熱愛編程的老程序猿 `

const message_3 = Path: ssml\r\nX-RequestId: ${XConnectionId}\r\nX-Timestamp: ${getXTime()}\r\nContent-Type: application/ssml+xml\r\n\r\n${SSML}

connect.send(message_3); ```

接收二進制消息拼接mp3

當三次發送結束後我們通過connect.on('binary')監聽websocket接收的二進制消息。

創建一個空的Buffer對象final_data,然後將每一次接收到的二進制內容拼接到final_data裏,一旦監聽到普通文本消息中包含Path:turn.end標識時則將final_data寫入創建一個mp3文件中。

let final_data=Buffer.alloc(0); connect.on("text", (data) => { if(data.indexOf("Path:turn.end")>=0){ fs.writeFileSync("test.mp3",final_data); connect.close(); } }) connect.on("binary", function (response) { let data = Buffer.alloc(0); response.on("readable", function () { const newData = response.read() if (newData)data = Buffer.concat([data, newData], data.length+newData.length); }) response.on("end", function () { const index = data.toString().indexOf("Path:audio")+12; final_data = Buffer.concat([final_data,data.slice(index)]); }) });

這樣我們就成功的保存出了mp3音頻文件,連Azure官網都不用打開!

命令行工具

我已經將整個代碼打包成一個命令行工具,使用非常簡單

npm install -g mstts-js mstts -i 文本轉語音 -o ./test.mp3

已全部開源: https://github.com/ezshine/mstts-js

在uni-app中使用

新建一個雲函數

新建一個雲函數,命名為mstts image.png

由於mstss-js已經封裝好了,只需要在雲函數中npm install mstts-js然後require即可,代碼如下 ``` 'use strict'; const mstts = require('mstts-js')

exports.main = async (event, context) => { const res = await mstts.getTTSData('要轉換的文本','CN-Yunxi');

//res為buffer格式

}); ```

下載播放mp3文件

要在uniapp中播放這個mp3格式的文件,有兩種方法

方法1. 先上傳到雲存儲,通過雲存儲地址訪問

``` exports.main = async (event, context) => { const res = await mstts.getTTSData('要轉換的文本','CN-Yunxi');

//res為buffer格式
var uploadRes = await uniCloud.uploadFile({
    cloudPath: "xxxxx.mp3",
    fileContent: res
})

return uploadRes.fileID;

}); ```

前端用法: uniCloud.callFunction({ name:"mstts", success:(res)=>{ const aud = uni.createInnerAudioContext(); aud.autoplay = true; aud.src = res; aud.play(); } })

  • 優點:雲函數安全
  • 缺點:文件上傳到雲存儲不做清理機制的話會浪費空間

方法2. 利用雲函數的URL化+集成響應來訪問

這種方法就是直接將雲函數的響應體變成一個mp3文件,直接通過audio.src賦值即可訪問`

``` exports.main = async (event, context) => { const res = await mstts.getTTSData('要轉換的文本','CN-Yunxi');

return {
    mpserverlessComposedResponse: true,
    isBase64Encoded: true,
    statusCode: 200,
    headers: {
        'Content-Type': 'audio/mp3',
        'Content-Disposition':'attachment;filename=\"temp.mp3\"'
    },
    body: res.toString('base64')
}

}; ```

前端用法: const aud = uni.createInnerAudioContext(); aud.autoplay = true; aud.src = 'https://ezshine-274162.service.tcloudbase.com/mstts'; aud.play();

  • 優點:用起來很簡單,無需保存文件到雲存儲
  • 缺點:URL化後的雲函數如果沒有安全機制,被抓包後可被其他人肆意使用

小結

這麼好用的tts庫,如果對你有所幫助別忘了在github裏點個star支持一下。


我是大帥,一個熱愛編程的程序。個人微信:dashuailaoyuan