在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("http://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("http://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

已全部開源: http://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 = 'http://ezshine-274162.service.tcloudbase.com/mstts'; aud.play();

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

小結

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


我是大帥,一個熱愛程式設計的程式。個人微信:dashuailaoyuan