在uni-app中使用微軟的文字轉語音服務
我正在參加跨端技術專題徵文活動,詳情檢視:juejin.cn/post/710123…
前言
嘗試過各種TTS的方案,一番體驗下來,發現微軟才是這個領域的王者,其Azure文字轉語音
服務的轉換出的語音效果最為自然,但Azure是付費服務,註冊操作付費都太麻煩了。但在其官網上竟然提供了一個完全體
的演示功能,能夠完完整整的體驗所有角色語音,說話風格...
但就是不能下載成mp3
檔案,所以有一些小夥伴逼不得已只好通過轉錄電腦的聲音來獲得音訊檔案,但這樣太麻煩了。其實,能在網頁裡看到聽到的所有資源,都是解密後的結果。也就是說,只要這個聲音從網頁裡播放出來了,我們必然可以找到方法提取到音訊檔案。
本文就是記錄了這整個探索實現的過程,請盡情享用~
本文大部分內容寫於今年年初一直按在手裡未釋出,我深知這個方法一旦公之於眾,可能很快會迎來微軟的封堵,甚至直接取消網頁體驗的入口和相關介面。
解析Azure官網的演示功能
使用Chrome瀏覽器開啟除錯面板,當我們在Azure官網中點選播放
功能時,可以從network標籤中監控到一個wss://
的請求,這是一個websocket
的請求。
兩個引數
在請求的URL
中,我們可以看到有兩個引數分別是Authorization
和X-ConnectionId
有意思的是,第一個引數就在網頁的原始碼裡,使用axios
對這個Azure文字轉語音的網址發起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 token = RegExp.$1; } ```
通過檢視發起請求的JS呼叫棧,加入斷點後再次點選播放
可以發現第二個引數X-ConnectionId
來自一個createNoDashGuid
的函式
this.privConnectionId = void 0 !== t ? t : s.createNoDashGuid(),
這就是一個uuid v4
格式的字串,nodash
就是沒有-
的意思。
三次傳送
請求時URL裡的兩個引數已經搞定了,我們繼續分析這個webscoket
請求,從Message標籤中可以看到
每次點選播放時,都向伺服器上報了三次資料,明顯可以看出來三次上報資料各自的作用
第一次的資料: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
的訊息的最後一條,都有明確的結束識別符號
turn.end
代表轉換結束!
用Node.js實現它
既然都解析出來了,剩下的就是在Node.js
中重新實現這個過程。
兩個引數
- 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; } ```
- 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
由於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
- 2022,38歲,裸辭,自由職業一年實況分享
- 太強了!外國小哥花16個月用Three.JS打造了一個無縫切地圖的3D開車遊戲
- 細節狂魔,用 JavaScript 復原何同學B站頭圖的創意
- 碼上摸金,用PIXI GSAP仿寫vanmoof剎車動效 | 猿創營
- 在uni-app中使用微軟的文字轉語音服務
- 萬馬奔騰隊的phaser3 戰疫小遊戲開發歷程回顧
- 用uni-app開發一個名為漢兜的遊戲
- 淺談對貪食蛇遊戲的一點微創新
- 手把手教你做iOS逆向分析,突破微信的群發多選數量限制
- 產品經理:你能不能用div給我畫條龍?
- 這個榜單我不服!終究還是錯付了這個綠茶掘金
- 前端摸魚神器,設計稿一鍵匯出 「小程式/Vue/Uni-app」程式碼
- 請收下這份原始碼,用Vue開發的一個“螞蟻森林澆水偷菜”遊戲
- 微信小程式統一分享,全域性接管頁面分享訊息的一些技巧
- 這45道面試可能被問到的JS判斷題!你能答對幾道?
- CSS邊玩邊學,這五個遊戲讓你對CSS的掌握更進一步!
- Web網站掃【小程式碼】登入的技術實現!記得收藏,要用時別找不到!
- 獻給所有技術內容創作者~猿創聚合助手小程式開發難點解析
- 花60秒給Vue3提的PR,竟然被尤大親自Merge了~
- 使用Vue開發“螞蟻森林澆水偷菜”遊戲的心得體會